develop to master (#4718)

* Remove dead code

* Localize TV requests messages on TV details page

* Transform buttons with link into anchors

* Sonarr sync: stop using seasonpass API

* chore(release): 🚀 v4.16.13

* Fix requests when 4k available and 4k disabled

Fixes #4610

* chore(release): 🚀 v4.16.14

* Hide subscribe button when request is available

* Hide subscribe button when request is denied

* Add Title to Partially Available Message

If the Title of the show is not menitoned it can be unclear what Episodes are now available.

* Better error message when test email fails due to missing recipient

* feat(discover): Add original language filter

* chore(release): 🚀 v4.16.15

* fix(4616): 🐛 fixed mandatory fields

* chore: 👥 Updated Contributors [skip ci]

* chore(release): 🚀 v4.16.16

* added test results into the PR pipeline

* chore(release): 🚀 v4.16.17

* Add information about cache refresh

* Update pr.yml

[skip ci]

* Update pr.yml

[skip ci]

* Update pr.yml

[skip ci]

* chore(release): 🚀 v4.17.0

* feat(discover): Add new trending source experimental feature

* fix(settings): Allow toggling features when there are more than one

* fix(discover): Fix new trending feature detection

* fix(discover): Fix cache mix up

* refactor(discover): Move movie trending feature toggle to backend

* feat(discover): Default trending source to new logic

* chore(release): 🚀 v4.18.0

* feat(sync): Detect reidentified movies in Emby and Jellyfin

* feat(sync): Detect reidentified series in Emby and Jellyfin

* Fix sync log criticity

* Update pr.yml

[skip ci]

* Update label.yml

[skip ci]

* Fix formatting

* Update pr.yml

[skip ci]

* Update label.yml

[skip ci]

* chore: 👥 Updated Contributors [skip ci]

* chore(release): 🚀 v4.19.0

* refactor(newsletter): Clarify very rare cases where newsletter doesn't publish a series

* refactor(newsletter): Clarify very rare cases where newsletter doesn't publish movie

* chore(release): 🚀 v4.19.1

* feat(discover): Show more relevant shows in upcoming TV

* chore(release): 🚀 v4.20.0

* fix(sync): Emby+Jellyfin - sync multi-episode files of 3+ episodes

* perf(sync): Emby+Jellyfin - use a more reliable filter to missing items

* fix(sync): Emby+Jellyfin - sync multi-episode files of 3+ episodes [skip ci]

* fix: added media type tag to media type text (#4638)

[skip ci]

* fix(sickrage): Fixed issue with incorrect handling of SiCKRAGE episode results returned during episode status changes, now expects array of objects from data path if present (#4648)

[skip ci]

* fix: Missing Poster broken link fix (#4637)

[skip ci]

* 🌐 Translations Update (#4622)

[skip ci]

* Update launch.json (#4650)

[skip ci]

* fix: Improve Swagger documentation (#4652)

* Upgrade Swashbuckle dependency

* Document /token response

* Add support for Newtonsoft annotations in Swagger

* Remove unecessary ActionResult [skip ci]

* fix(API): Fix pagination in some edge cases (#4649)

[skip ci]

* 🌐 Translations Update (#4655)

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(discover): Carousel touch not working when scrolling page and recommendations and similar movie navigation (#4633)

* fixed touch not working on carousels

* fixed touch not working

* Movie details component fixes

Fixed recommendations and similar not changing the data on the component by calling the init function again on param change

Moved the ngif results > 0 to the mat-expansion panel to avoid rendering  the entire element if it doesn't have any results instead of having an empty panel.

* removed unused line, added scroll to top on init

* updated recommendation refresh implementation

Changed the implementation to use the router instead in order to reload the component instead of just reloading the data.

This implementation makes sure the component gets destroyed on navigation eliminating any memory leaks, reloading CSS in case of having animations on page load and generally a continuation of the experience you get when you browse into a movie from the discover page.

* chore: 👥 Updated Contributors [skip ci]

* chore(release): 🚀 v4.20.1 [skip ci]

* fix: 🐛 Fixed the Request on Behalf of having blanks (#4667)

* chore(release): 🚀 v4.20.2 [skip ci]

* fix(plex): 🐛 Fixed an issue with the Plex Sync

* chore(release): 🚀 v4.20.3 [skip ci]

* fix (technical): Improved some of the date time parsing handling

* fix: fixed build

* chore(release): 🚀 v4.20.4 [skip ci]

* feat: Upgrade to Angular14 (#4668)

* refactor: 🔥 removed angular-bootstrap-md dependancy

* chore: update tsconfig

* yeah

* ng14 upgrade

* refactor: migration changes

* fix: fixed CLI

* test: Fixed automation

* chore: 👥 Updated Contributors [skip ci]

* perf: stop populating obsolete subscribe fields (#4625)

* chore(release): 🚀 v4.21.0 [skip ci]

* fix(images): Retry images with a backoff when we get a Too Many requests from TheMovieDb #4685

* chore(release): 🚀 v4.21.1 [skip ci]

* 🌐 Translations Update (#4683)

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix: Landing and Login page improvements (#4690)

* chore(release): 🚀 v4.21.2 [skip ci]

* feat(discover):  Added infinite scroll on advanced search results

* feat(discover):  Added infinite scroll on advanced search results

* chore(release): 🚀 v4.22.0 [skip ci]

* 🌐 Translations Update (#4694)

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(discover): 🐛 Created new Image component to handle 429's from TMDB (#4698) and fixed #4635 (#4699)

* chore(release): 🚀 v4.22.1 [skip ci]

* fix: fixed an issue where I broke images for some users

* chore(release): 🚀 v4.22.2 [skip ci]

* ci(Mergify): configuration update (#4701)

Signed-off-by: Jamie <tidusjar@gmail.com> [skip ci]

* fix: Override Sonarr V3 Profiles endpoint (#4678)

* Override Sonarr V3 Profiles endpoint [skip ci]

* fix(4K) :4K request fixes (#4702)

* GetRequestsByStatus wasn't implementing the MovieRequests object correctly for 4K quality requests with the ProcessingRequest status.

* Fixed 4K requests not getting automatically approved if the user has the "Auto Approve Movie" role flag enabled.

* Fixed "Request Date" values for the "left-panel-details" div class. Previously when the movie was exclusively 4K (regular request was absent), then "Request Date" equaled DateTime.MinValue (January 1, 0001).

* Fixed "Request Status" evaluation in the "left-panel-details" div class. Now it shows the appropriate status instead of an empty spot. "Request Status" displays both regular and 4K statuses at the same time if needed. Added a comma to the end of the "RequestStatus" label to maintain design consistency with the other labels. Also added a "Denied Reason" element for 4K  requests.

* chore: 👥 Updated Contributors [skip ci]

* chore(release): 🚀 v4.22.3 [skip ci]

* chore: Storybook (#4700)

[skip ci]

* chore: Translations

[skip ci]

* 🌐 Translations Update (#4704)

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci]

* fix(translations): 🌐 New translations from Crowdin [skip ci] (#4713)

* build: Run automation tests in docker (#4715)

[skip ci]

* fix: fixed trakt image not loading when base url present (#4711)

[skip ci]

* fix: 🐛 Fixed missing externals (#4712)

* chore: 👥 Updated Contributors [skip ci]

* chore(release): 🚀 v4.22.4 [skip ci]

* test: fixed automationt tests [skip ci]

Co-authored-by: sephrat <34862846+sephrat@users.noreply.github.com>
Co-authored-by: Conventional Changelog Action <conventional.changelog.action@github.com>
Co-authored-by: Teifun2 <Teifun2@users.noreply.github.com>
Co-authored-by: contrib-readme-bot <contrib-readme-action@noreply.com>
Co-authored-by: dr3amer <91037083+dr3am37@users.noreply.github.com>
Co-authored-by: echel0n <echel0n@sickrage.ca>
Co-authored-by: Marley <55280588+marleypowell@users.noreply.github.com>
Co-authored-by: Igor Borges <igor@borges.dev>
Co-authored-by: Lucane <Lucane@users.noreply.github.com>
Co-authored-by: mkgeeky <github@mkgeeky.xyz>
plex-watchlist-error-reporting
Jamie 2 years ago committed by GitHub
parent 48279b48ce
commit bb781f2787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,47 @@
name: 'Chromatic'
# Event for the workflow
on:
push:
workflow_dispatch:
# List of jobs
jobs:
storybook-build:
# Operating System
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: NodeModules Cache
uses: actions/cache@v2
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
working-directory: ./src/Ombi/ClientApp
run: yarn
- name: Publish to Chromatic
if: github.ref != 'refs/heads/master'
uses: chromaui/action@v1
with:
projectToken: 7c47e1a1a4bd
exitZeroOnChanges: true
workingDir: ./src/Ombi/ClientApp
buildScriptName: storybookbuild
exitOnceUploaded: true
- name: Publish to Chromatic and auto accept changes
if: github.ref == 'refs/heads/develop'
uses: chromaui/action@v1
with:
projectToken: 7c47e1a1a4bd
autoAcceptChanges: true # 👈 Option to accept all changes
workingDir: ./src/Ombi/ClientApp
buildScriptName: storybookbuild
exitOnceUploaded: true

@ -34,15 +34,29 @@ jobs:
- name: Install Frontend Deps
run: yarn --cwd ./src/Ombi/ClientApp install
- name: Install Automation Deps
run: yarn --cwd ./tests install
- name: Start Backend
run: |
nohup dotnet run --project ./src/Ombi -- --host http://*:3577 &
- name: Start Frontend
run: |
nohup yarn --cwd ./src/Ombi/ClientApp start &
- name: Build Frontend
run: yarn --cwd ./src/Ombi/ClientApp build
- name: Build Docker Image
run: docker build -t ombi src/
- name: Run Docker Image
run: nohup docker run --rm -p 5000:5000 ombi &
- name: Sleep for server to start
run: sleep 20
# - name: Start Frontend
# run: |
# nohup yarn --cwd ./src/Ombi/ClientApp start &
# - name: Install Automation Deps
# run: yarn --cwd ./tests install
# - name: Start Backend
# run: |
# nohup dotnet run --project ./src/Ombi -- --host http://*:3577 &
- name: Cypress Tests
uses: cypress-io/github-action@v2.8.2
with:
@ -50,9 +64,9 @@ jobs:
browser: chrome
headless: true
working-directory: tests
wait-on: http://localhost:3577/
# 7 minutes
wait-on-timeout: 420
wait-on: http://localhost:5000/
# 10 minutes
wait-on-timeout: 600
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -8,6 +8,8 @@
name: Labeler
on: [pull_request]
permissions: write-all
jobs:
label:

@ -5,6 +5,11 @@ on:
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
pull-requests: write
issues: write
repository-projects: write
jobs:
build-ui:
runs-on: ubuntu-latest
@ -28,6 +33,7 @@ jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1
@ -45,7 +51,7 @@ jobs:
- name: Run Unit Tests
run: |
cd src
dotnet test --logger trx --results-directory "TestResults"
dotnet test --configuration "Release" --logger "trx;LogFileName=test-results.trx"
analysis:
runs-on: ubuntu-latest

1
.gitignore vendored

@ -251,3 +251,4 @@ _Pvt_Extensions
/src/Ombi/databases.json
/src/Ombi/healthchecksdb
/src/Ombi/ClientApp/package-lock.json
/src/Ombi.Core/Properties/launchSettings.json

@ -0,0 +1,7 @@
pull_request_rules:
- name: Automatic merge on approval
conditions:
- "#approved-reviews-by>=1"
actions:
merge:
method: merge

@ -1,360 +1,363 @@
## [4.16.12](https://github.com/Ombi-app/Ombi/compare/v4.16.11...v4.16.12) (2022-04-19)
## [4.16.11](https://github.com/Ombi-app/Ombi/compare/v4.16.10...v4.16.11) (2022-04-14)
## [4.22.4](https://github.com/Ombi-app/Ombi/compare/v4.22.3...v4.22.4) (2022-08-04)
### Bug Fixes
* Set the default job for the watchlist import to hourly instead of daily ([75906af](https://github.com/Ombi-app/Ombi/commit/75906af0adee3e3c68d825c3aaa8f7b918461b1f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4fc2c1f](https://github.com/Ombi-app/Ombi/commit/4fc2c1f24534085a783a3d5791f5533b68272153))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76ab733](https://github.com/Ombi-app/Ombi/commit/76ab733b91791e4d93d184f3c7d0779c6a388695))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([06e4cef](https://github.com/Ombi-app/Ombi/commit/06e4cefa7b4e55b860da9a64f461f6ec8fa17367))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c12d89d](https://github.com/Ombi-app/Ombi/commit/c12d89d6781a337520977ad285f8d08c93f434dd))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bc0c2f6](https://github.com/Ombi-app/Ombi/commit/bc0c2f622e34fb5a2711039d9ed7aad34f982b15))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([e4b00e6](https://github.com/Ombi-app/Ombi/commit/e4b00e6b3468bd9389eeb02fc6ad7daf27abc3b3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1998d3](https://github.com/Ombi-app/Ombi/commit/d1998d326f999a38586d0a351a20c5448df95842))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bee4ccb](https://github.com/Ombi-app/Ombi/commit/bee4ccb804594e7385b1fbdc9fe2ef5c42e0d21f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([80233ed](https://github.com/Ombi-app/Ombi/commit/80233ed560cc976e83570d0655c3472f20171fb3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a78adc](https://github.com/Ombi-app/Ombi/commit/8a78adc9bb62f277f2b213dcb3847ed6d0089fcb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d04c60a](https://github.com/Ombi-app/Ombi/commit/d04c60aa5909b47ba6bffa6f66b03079cbd43521))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([92a785e](https://github.com/Ombi-app/Ombi/commit/92a785e736fa4b72a45270da2d0f4661df433078))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([634982d](https://github.com/Ombi-app/Ombi/commit/634982df2661cefab5ea9f5163fe04a005cc0171))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([b404baa](https://github.com/Ombi-app/Ombi/commit/b404baad6d0aeaa1561701e0db8db4e78613a364))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d14f11e](https://github.com/Ombi-app/Ombi/commit/d14f11e0eb20ab0a68e765ee77968b3b3e54e995))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7cf64f9](https://github.com/Ombi-app/Ombi/commit/7cf64f909d78908edaabeffb8a39a7d02e73fe7e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c9e1ec](https://github.com/Ombi-app/Ombi/commit/0c9e1ec090827080cc8f7393e5e91456ff37d691))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([3b0b730](https://github.com/Ombi-app/Ombi/commit/3b0b730cb02efe24f6d4026e5fdb20d37e495119))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6ed1a03](https://github.com/Ombi-app/Ombi/commit/6ed1a03b7ff4077f09ea9e13394b18b0d138f4c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([2941acd](https://github.com/Ombi-app/Ombi/commit/2941acd3b2ec74a5e6aeea275ab5a39d2653f37f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c075a1a](https://github.com/Ombi-app/Ombi/commit/c075a1a66784d975eaf60f2dfbbcbe048f2f63d7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76bd81c](https://github.com/Ombi-app/Ombi/commit/76bd81c3ca55a98c6ec944a838dc01294a6193a6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0d38275](https://github.com/Ombi-app/Ombi/commit/0d3827507e002bcf58f673e97ffcc3bd25dcf337))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5c99601](https://github.com/Ombi-app/Ombi/commit/5c99601b07aec1a65d0186a4c4327440811e64c6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01546a0](https://github.com/Ombi-app/Ombi/commit/01546a0f7f86379528b486463246ef9bdfb9033e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d7fea78](https://github.com/Ombi-app/Ombi/commit/d7fea7843aaaab7ddff8dc31ca6d2a9117471dcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([1a6b95d](https://github.com/Ombi-app/Ombi/commit/1a6b95d45c220310213b8d811272a63f0f6ff42b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([fa10174](https://github.com/Ombi-app/Ombi/commit/fa1017422c4efd4b0897871bd3c671151774d7c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c31e62](https://github.com/Ombi-app/Ombi/commit/0c31e628df376aac6d56ae67c7c705a9a4a7c080))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6399643](https://github.com/Ombi-app/Ombi/commit/63996437a02fe10ffae6822ffa15369bec0a6b36))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5826e2d](https://github.com/Ombi-app/Ombi/commit/5826e2d9a1c3f1210a87fa270dc0c81bac32944a))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d434514](https://github.com/Ombi-app/Ombi/commit/d43451405be489254d7cdc7755d5f516a1e495a5))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0b9596d](https://github.com/Ombi-app/Ombi/commit/0b9596d807178f5e071113ec0347868ec7f0960b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8c4c0b2](https://github.com/Ombi-app/Ombi/commit/8c4c0b262978c1303767af360d802c4b4c2b4d24))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([289ab77](https://github.com/Ombi-app/Ombi/commit/289ab77b0e04aae235b6f6cebc86e0a8d1f0cf2b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([30e3417](https://github.com/Ombi-app/Ombi/commit/30e3417285a4eed18d429d7776f0e74096e834c0))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6c0a5da](https://github.com/Ombi-app/Ombi/commit/6c0a5dadd4b8f37760252eb0fe7f88908f55506d))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d5bf969](https://github.com/Ombi-app/Ombi/commit/d5bf9692ce1fc0ccfe7beca6dd200c78be177bdc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a9e7ea](https://github.com/Ombi-app/Ombi/commit/8a9e7ea588aefbcd73ed82625887e3614e1703ea))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01047a3](https://github.com/Ombi-app/Ombi/commit/01047a3fd67153f3ff16f860d2c7b50213e8d9b2))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([698a23f](https://github.com/Ombi-app/Ombi/commit/698a23fb83f323cdd1dd57cb49803079d44214a7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([24eb842](https://github.com/Ombi-app/Ombi/commit/24eb842fc4424f7bcc3ec2949d7f5472492e96f6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([ac8b16a](https://github.com/Ombi-app/Ombi/commit/ac8b16a3051ad71dbd54a8973c7dd847b564a515))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f428ce6](https://github.com/Ombi-app/Ombi/commit/f428ce6a700c081437703839bc84d2f2b1138bcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([94b16df](https://github.com/Ombi-app/Ombi/commit/94b16dfe09bf1d2cd6286777d74eb5d4496abbbb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4881775](https://github.com/Ombi-app/Ombi/commit/4881775eda69a8f136ce0d8fbbf970e3d0406dc9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8297db9](https://github.com/Ombi-app/Ombi/commit/8297db91e85da308bde6fb09ad78347dee063630))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1152ab](https://github.com/Ombi-app/Ombi/commit/d1152ab7674243daa528c524c0cdc87d81ad49c9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([eb2788b](https://github.com/Ombi-app/Ombi/commit/eb2788b761b55c487a59a049427ca08f6c10e836))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([21a794c](https://github.com/Ombi-app/Ombi/commit/21a794cbc0a5fa735ca0347c8f7f1ac04a487fbc))
* :bug: Fixed missing externals ([#4712](https://github.com/Ombi-app/Ombi/issues/4712)) ([fcc1eaa](https://github.com/Ombi-app/Ombi/commit/fcc1eaaa377683dcdc81d62a2a688fb0c4490c7b))
* fixed trakt image not loading when base url present ([#4711](https://github.com/Ombi-app/Ombi/issues/4711)) ([f102dcf](https://github.com/Ombi-app/Ombi/commit/f102dcf751c2eb62ebfe30f9f8e4b2ad863c3b0d))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4713](https://github.com/Ombi-app/Ombi/issues/4713)) ([ff142b0](https://github.com/Ombi-app/Ombi/commit/ff142b09abbb2f9540387284222552e6e12639fe))
## [4.10.2](https://github.com/Ombi-app/Ombi/compare/v4.10.1...v4.10.2) (2022-01-22)
## [4.22.3](https://github.com/Ombi-app/Ombi/compare/v4.22.2...v4.22.3) (2022-07-28)
### Bug Fixes
## [4.16.10](https://github.com/Ombi-app/Ombi/compare/v4.16.9...v4.16.10) (2022-04-13)
* Override Sonarr V3 Profiles endpoint ([#4678](https://github.com/Ombi-app/Ombi/issues/4678)) ([875da95](https://github.com/Ombi-app/Ombi/commit/875da959f353119b05138d68ee6d32a49e14b91e))
## [4.16.9](https://github.com/Ombi-app/Ombi/compare/v4.16.8...v4.16.9) (2022-04-13)
## [4.22.2](https://github.com/Ombi-app/Ombi/compare/v4.22.1...v4.22.2) (2022-07-25)
### Bug Fixes
* **plex-watchlist:** Only request the latest season when importing from the watchlist ([77a47ff](https://github.com/Ombi-app/Ombi/commit/77a47ff157c6c5feafe3f2a29a3fcba8df4fdfef))
* fixed an issue where I broke images for some users ([81ddc85](https://github.com/Ombi-app/Ombi/commit/81ddc8553b9094c3f6843b036daebb2eb9262e00))
## [4.16.8](https://github.com/Ombi-app/Ombi/compare/v4.16.7...v4.16.8) (2022-04-13)
## [4.22.1](https://github.com/Ombi-app/Ombi/compare/v4.22.0...v4.22.1) (2022-07-25)
### Bug Fixes
* **availability:** Fixed an issue where we wouldn't mark a available 4k movie as available (when 4K request feature is disabled) ([b492699](https://github.com/Ombi-app/Ombi/commit/b49269961d4830a530e3054976a47f519524948b))
* **discover:** :bug: Created new Image component to handle 429's from TMDB ([#4698](https://github.com/Ombi-app/Ombi/issues/4698)) and fixed [#4635](https://github.com/Ombi-app/Ombi/issues/4635) ([#4699](https://github.com/Ombi-app/Ombi/issues/4699)) ([f22d3da](https://github.com/Ombi-app/Ombi/commit/f22d3da765799365455b919027f7563e52b347c3))
## [4.16.7](https://github.com/Ombi-app/Ombi/compare/v4.16.6...v4.16.7) (2022-04-12)
# [4.22.0](https://github.com/Ombi-app/Ombi/compare/v4.21.2...v4.22.0) (2022-07-22)
### Features
## [4.16.6](https://github.com/Ombi-app/Ombi/compare/v4.16.5...v4.16.6) (2022-04-11)
* **discover:** ✨ Added infinite scroll on advanced search results ([898bc89](https://github.com/Ombi-app/Ombi/commit/898bc89fa78245c1f3de9481f6c724f087a16e39))
## [4.16.5](https://github.com/Ombi-app/Ombi/compare/v4.16.4...v4.16.5) (2022-04-08)
## [4.21.2](https://github.com/Ombi-app/Ombi/compare/v4.21.1...v4.21.2) (2022-07-22)
### Bug Fixes
* **watchlist:** actually fixed it this time... ([d962a32](https://github.com/Ombi-app/Ombi/commit/d962a3211eca29520662ddce962676e3aea17ec5))
* Landing and Login page improvements ([#4690](https://github.com/Ombi-app/Ombi/issues/4690)) ([6d423b5](https://github.com/Ombi-app/Ombi/commit/6d423b5447c52c5e59d8d2bd92a23b47468eb736))
## [4.16.4](https://github.com/Ombi-app/Ombi/compare/v4.16.3...v4.16.4) (2022-04-08)
## [4.21.1](https://github.com/Ombi-app/Ombi/compare/v4.21.0...v4.21.1) (2022-07-11)
### Bug Fixes
## [4.16.3](https://github.com/Ombi-app/Ombi/compare/v4.16.2...v4.16.3) (2022-04-08)
* **images:** Retry images with a backoff when we get a Too Many requests from TheMovieDb [#4685](https://github.com/Ombi-app/Ombi/issues/4685) ([3f1f35d](https://github.com/Ombi-app/Ombi/commit/3f1f35df3164db6739691cdda8f925c296239791))
### Bug Fixes
* **plex-watchlist:** :bug: Fixed the issue where the watchlist didn't work for users logging in via OAuth ([6398f6a](https://github.com/Ombi-app/Ombi/commit/6398f6a4f7755281ebeac537e3ff623df5cfa0f3))
# [4.21.0](https://github.com/Ombi-app/Ombi/compare/v4.20.4...v4.21.0) (2022-06-22)
### Features
## [4.16.2](https://github.com/Ombi-app/Ombi/compare/v4.16.1...v4.16.2) (2022-04-07)
* Upgrade to Angular14 ([#4668](https://github.com/Ombi-app/Ombi/issues/4668)) ([b9d55a4](https://github.com/Ombi-app/Ombi/commit/b9d55a469b412558cbf67c1e25db7fdda5964cd8))
### Bug Fixes
### Performance Improvements
* **wizard:** Fixed an issue when using Plex OAuth it could fail setting up ([b743cf4](https://github.com/Ombi-app/Ombi/commit/b743cf4fafa7341ad1b163276f006d7ab0e9dcff))
* stop populating obsolete subscribe fields ([#4625](https://github.com/Ombi-app/Ombi/issues/4625)) ([9a73463](https://github.com/Ombi-app/Ombi/commit/9a734637665f671b17c2bb440d93b35a891c142b))
## [4.16.1](https://github.com/Ombi-app/Ombi/compare/v4.16.0...v4.16.1) (2022-04-07)
## [4.20.4](https://github.com/Ombi-app/Ombi/compare/v4.20.3...v4.20.4) (2022-06-15)
### Bug Fixes
# [4.16.0](https://github.com/Ombi-app/Ombi/compare/v4.15.6...v4.16.0) (2022-04-07)
* fixed build ([f877921](https://github.com/Ombi-app/Ombi/commit/f8779219146051ea74f8b6408658ff7975afb88b))
## [4.15.6](https://github.com/Ombi-app/Ombi/compare/v4.15.5...v4.15.6) (2022-04-07)
## [4.20.3](https://github.com/Ombi-app/Ombi/compare/v4.20.2...v4.20.3) (2022-06-05)
### Bug Fixes
* **radarr:** Fixed an issue where we couldn't sync radarr content [#4577](https://github.com/Ombi-app/Ombi/issues/4577) ([a5355a3](https://github.com/Ombi-app/Ombi/commit/a5355a3023e6900c4dd1b0da4722d7596c03907f))
* **plex:** 🐛 Fixed an issue with the Plex Sync ([ab1a11a](https://github.com/Ombi-app/Ombi/commit/ab1a11af78efbe9d37bd55aa80a640796c138a98))
## [4.15.5](https://github.com/Ombi-app/Ombi/compare/v4.15.4...v4.15.5) (2022-04-06)
## [4.20.2](https://github.com/Ombi-app/Ombi/compare/v4.20.1...v4.20.2) (2022-06-03)
### Bug Fixes
## [4.15.4](https://github.com/Ombi-app/Ombi/compare/v4.15.3...v4.15.4) (2022-03-29)
* :bug: Fixed the Request on Behalf of having blanks ([#4667](https://github.com/Ombi-app/Ombi/issues/4667)) ([7dd9b1c](https://github.com/Ombi-app/Ombi/commit/7dd9b1cac07f571dd35b362544e4fe0226c4b817))
## [4.15.3](https://github.com/Ombi-app/Ombi/compare/v4.15.2...v4.15.3) (2022-03-24)
## [4.20.1](https://github.com/Ombi-app/Ombi/compare/v4.20.0...v4.20.1) (2022-05-27)
### Bug Fixes
## [4.15.2](https://github.com/Ombi-app/Ombi/compare/v4.15.1...v4.15.2) (2022-03-23)
* added media type tag to media type text ([#4638](https://github.com/Ombi-app/Ombi/issues/4638)) ([fe501d3](https://github.com/Ombi-app/Ombi/commit/fe501d34a0c36ac9f000b107eca49dbc6694d006))
* **API:** Fix pagination in some edge cases ([#4649](https://github.com/Ombi-app/Ombi/issues/4649)) ([a70bf8f](https://github.com/Ombi-app/Ombi/commit/a70bf8f46c76d74c9dfdf908c53bd9955ca0a35d))
* **discover:** Carousel touch not working when scrolling page and recommendations and similar movie navigation ([#4633](https://github.com/Ombi-app/Ombi/issues/4633)) ([d5ef1d5](https://github.com/Ombi-app/Ombi/commit/d5ef1d53e5f77d19dba8b8059c80b538a3e14f2a))
* Improve Swagger documentation ([#4652](https://github.com/Ombi-app/Ombi/issues/4652)) ([181892b](https://github.com/Ombi-app/Ombi/commit/181892bcfe88e6d76febf49ef57745d04552d08e))
* Missing Poster broken link fix ([#4637](https://github.com/Ombi-app/Ombi/issues/4637)) ([4070f0d](https://github.com/Ombi-app/Ombi/commit/4070f0d093b1c92487a1c80cabad8283a9650f51))
* **sickrage:** Fixed issue with incorrect handling of SiCKRAGE episode results returned during episode status changes, now expects array of objects from data path if present ([#4648](https://github.com/Ombi-app/Ombi/issues/4648)) ([6d16442](https://github.com/Ombi-app/Ombi/commit/6d16442d4d714920367df065a3ced42b729f4233))
* **sync:** Emby+Jellyfin - sync multi-episode files of 3+ episodes ([bd8fd89](https://github.com/Ombi-app/Ombi/commit/bd8fd890554c9d85d6da4d2cee813e82ce698e52))
### Bug Fixes
* **metadata:** improved the metadata job to also lookup the media in Plex to see if it has any more uptodate metadata ([83d1a15](https://github.com/Ombi-app/Ombi/commit/83d1a15cc9d0ee91be73bd91c4672cf1bcf2728a))
# [4.20.0](https://github.com/Ombi-app/Ombi/compare/v4.19.1...v4.20.0) (2022-04-28)
### Features
## [4.15.1](https://github.com/Ombi-app/Ombi/compare/v4.15.0...v4.15.1) (2022-03-18)
* **discover:** Show more relevant shows in upcoming TV ([8357819](https://github.com/Ombi-app/Ombi/commit/8357819b53b8c675c0b246d7006b5a778bdba33f))
### Bug Fixes
* **mediaserver:** fixed an issue where we were not detecting available content correctly [#4542](https://github.com/Ombi-app/Ombi/issues/4542) ([9cdd6f4](https://github.com/Ombi-app/Ombi/commit/9cdd6f41cdab8825a984905c089611409c53c753))
## [4.19.1](https://github.com/Ombi-app/Ombi/compare/v4.19.0...v4.19.1) (2022-04-27)
# [4.15.0](https://github.com/Ombi-app/Ombi/compare/v4.14.4...v4.15.0) (2022-03-17)
# [4.19.0](https://github.com/Ombi-app/Ombi/compare/v4.18.0...v4.19.0) (2022-04-27)
### Bug Fixes
### Features
* **jellyfin:** :bug: Fixed an issue where Jellyfin content was showing the Play on Emby button ([18b167d](https://github.com/Ombi-app/Ombi/commit/18b167d16a3d682b5060ee36dedbbb069bef09de)), closes [#4542](https://github.com/Ombi-app/Ombi/issues/4542)
* **sync:** Detect reidentified movies in Emby and Jellyfin ([5938077](https://github.com/Ombi-app/Ombi/commit/5938077d82a5357f79c07b218b3986557a5816e8))
* **sync:** Detect reidentified series in Emby and Jellyfin ([9096e91](https://github.com/Ombi-app/Ombi/commit/9096e91d55d268819bce22831f8a8b27f2a1776b))
## [4.14.4](https://github.com/Ombi-app/Ombi/compare/v4.14.3...v4.14.4) (2022-03-10)
# [4.18.0](https://github.com/Ombi-app/Ombi/compare/v4.17.0...v4.18.0) (2022-04-26)
### Bug Fixes
* :bug: Fixed the Request On Behalf autocomplete not filtering correctly ([a8ba2f3](https://github.com/Ombi-app/Ombi/commit/a8ba2f3544a1c01c57f217c4036a277ab0e67a09)), closes [#4539](https://github.com/Ombi-app/Ombi/issues/4539)
* **translations:** 🌐 New translations from Crowdin [skip ci] ([356c742](https://github.com/Ombi-app/Ombi/commit/356c7424e0ce8c1c5063b04bc6ed9b809f214d65))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6fcaecf](https://github.com/Ombi-app/Ombi/commit/6fcaecf80b766f2d43ac7082d74364238e1638b7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([132f4d4](https://github.com/Ombi-app/Ombi/commit/132f4d4e609b7fb7e37f38ee2f395926e2911abe))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f292006](https://github.com/Ombi-app/Ombi/commit/f292006a08894a8d0ba899c8c6e9fe863e558dda))
* **discover:** Fix cache mix up ([03d9422](https://github.com/Ombi-app/Ombi/commit/03d94220c7eaafb50c6c80a6ed1150794b873ac3))
* **discover:** Fix new trending feature detection ([6794b88](https://github.com/Ombi-app/Ombi/commit/6794b887f6544fb41528bdd9728b7824b65e47ee))
* **settings:** Allow toggling features when there are more than one ([a373359](https://github.com/Ombi-app/Ombi/commit/a373359ae8e6bad42b558a6e01a8ff2840d3bbaa))
### Features
## [4.14.3](https://github.com/Ombi-app/Ombi/compare/v4.14.2...v4.14.3) (2022-03-06)
* **discover:** Add new trending source experimental feature ([1a0823c](https://github.com/Ombi-app/Ombi/commit/1a0823ca80559417c67323aaeaa1ef5243e98031))
* **discover:** Default trending source to new logic ([4f12939](https://github.com/Ombi-app/Ombi/commit/4f12939e22020a67a5ee75e2907923faea136e8d))
### Bug Fixes
* **availability:** :bug: Fixed an issue where with 4k content, we could repeat notifications ([f9ebc1c](https://github.com/Ombi-app/Ombi/commit/f9ebc1cc2e13c7cd335121cd86295b10eda529ba))
# [4.17.0](https://github.com/Ombi-app/Ombi/compare/v4.16.17...v4.17.0) (2022-04-25)
### Features
## [4.14.2](https://github.com/Ombi-app/Ombi/compare/v4.14.1...v4.14.2) (2022-03-05)
* **discover:** Add original language filter ([ef7ec86](https://github.com/Ombi-app/Ombi/commit/ef7ec861d8aede2a4817752c990617f583805391))
### Bug Fixes
* **Sonarr:** :bug: Fixed an issue where some seasons were not being monitored correctly in sonarr ([60cfd41](https://github.com/Ombi-app/Ombi/commit/60cfd41f68e9006555c1a419dcff1aaa24b3e09f)), closes [#4506](https://github.com/Ombi-app/Ombi/issues/4506)
## [4.16.17](https://github.com/Ombi-app/Ombi/compare/v4.16.16...v4.16.17) (2022-04-25)
## [4.14.1](https://github.com/Ombi-app/Ombi/compare/v4.14.0...v4.14.1) (2022-03-03)
## [4.16.16](https://github.com/Ombi-app/Ombi/compare/v4.16.15...v4.16.16) (2022-04-25)
### Bug Fixes
# [4.14.0](https://github.com/Ombi-app/Ombi/compare/v4.13.2...v4.14.0) (2022-03-02)
* **4616:** :bug: fixed mandatory fields ([d8f2260](https://github.com/Ombi-app/Ombi/commit/d8f2260c7ae3ed48386743b7adbd06e284487034))
## [4.13.2](https://github.com/Ombi-app/Ombi/compare/v4.13.1...v4.13.2) (2022-03-01)
## [4.16.15](https://github.com/Ombi-app/Ombi/compare/v4.16.14...v4.16.15) (2022-04-24)
### Bug Fixes
* **requests:** :bug: Fixed an issue where you couldn't approve movies from the request list ([1611ef9](https://github.com/Ombi-app/Ombi/commit/1611ef9198befbb7a4db50a4f0953e50f29a788f))
## [4.16.14](https://github.com/Ombi-app/Ombi/compare/v4.16.13...v4.16.14) (2022-04-19)
## [4.13.1](https://github.com/Ombi-app/Ombi/compare/v4.13.0...v4.13.1) (2022-03-01)
## [4.16.13](https://github.com/Ombi-app/Ombi/compare/v4.16.12...v4.16.13) (2022-04-19)
### Bug Fixes
* **details:** :bug: Fixed the missing Play on Media server button for 4k content [#4529](https://github.com/Ombi-app/Ombi/issues/4529) ([68600f3](https://github.com/Ombi-app/Ombi/commit/68600f3b45376e12dd2ef263d81ca4040c84cbca))
* **discover:** :bug: Fixed the issue where there was an option on the discover to request 4k shows (that's not supported currently) ([dcfd688](https://github.com/Ombi-app/Ombi/commit/dcfd688c8d2337e55fa9c6c33b7c3e80fc560cda))
* **requests:** :bug: Fixed the issue where we could no longer approve TV Requests from the requests list ([19fe4e3](https://github.com/Ombi-app/Ombi/commit/19fe4e342efe5578c26ab8ba7ee2f2e64bbc9418))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4526](https://github.com/Ombi-app/Ombi/issues/4526)) ([7e9f54f](https://github.com/Ombi-app/Ombi/commit/7e9f54fc80a09c938184e6be40ce5f49ce9673ef))
## [4.16.12](https://github.com/Ombi-app/Ombi/compare/v4.16.11...v4.16.12) (2022-04-19)
# [4.13.0](https://github.com/Ombi-app/Ombi/compare/v4.12.7...v4.13.0) (2022-02-25)
## [4.16.11](https://github.com/Ombi-app/Ombi/compare/v4.16.10...v4.16.11) (2022-04-14)
### Bug Fixes
* **4k:** Hide 'Has 4K Request' column list if 4k feature is disabled ([#4521](https://github.com/Ombi-app/Ombi/issues/4521)) ([a9a6067](https://github.com/Ombi-app/Ombi/commit/a9a60678e74d22fa7ba34051a2645db86b600b4a))
* **issues:** Fix label ID in chatbox page ([#4520](https://github.com/Ombi-app/Ombi/issues/4520)) ([76882ad](https://github.com/Ombi-app/Ombi/commit/76882adf231f92e1cdd396239933c13467c112b3))
* **localisation:** Localize request types in notifications ([#4516](https://github.com/Ombi-app/Ombi/issues/4516)) ([e09435d](https://github.com/Ombi-app/Ombi/commit/e09435da455b12fc429f129372de31e0654da797))
* **notifications:** Remove generic admin email in favour of admins' email ([#4519](https://github.com/Ombi-app/Ombi/issues/4519)) ([b90fc5f](https://github.com/Ombi-app/Ombi/commit/b90fc5fea771a83e6cf576c71a307066efd59ea4))
* **tv:** Display TV show as requested if all episodes are requested ([#4518](https://github.com/Ombi-app/Ombi/issues/4518)) ([2ed8c48](https://github.com/Ombi-app/Ombi/commit/2ed8c48d128a69f0d144c5d332286dbf3b0bdf28))
* Set the default job for the watchlist import to hourly instead of daily ([75906af](https://github.com/Ombi-app/Ombi/commit/75906af0adee3e3c68d825c3aaa8f7b918461b1f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4fc2c1f](https://github.com/Ombi-app/Ombi/commit/4fc2c1f24534085a783a3d5791f5533b68272153))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76ab733](https://github.com/Ombi-app/Ombi/commit/76ab733b91791e4d93d184f3c7d0779c6a388695))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([06e4cef](https://github.com/Ombi-app/Ombi/commit/06e4cefa7b4e55b860da9a64f461f6ec8fa17367))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c12d89d](https://github.com/Ombi-app/Ombi/commit/c12d89d6781a337520977ad285f8d08c93f434dd))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bc0c2f6](https://github.com/Ombi-app/Ombi/commit/bc0c2f622e34fb5a2711039d9ed7aad34f982b15))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([e4b00e6](https://github.com/Ombi-app/Ombi/commit/e4b00e6b3468bd9389eeb02fc6ad7daf27abc3b3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1998d3](https://github.com/Ombi-app/Ombi/commit/d1998d326f999a38586d0a351a20c5448df95842))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bee4ccb](https://github.com/Ombi-app/Ombi/commit/bee4ccb804594e7385b1fbdc9fe2ef5c42e0d21f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([80233ed](https://github.com/Ombi-app/Ombi/commit/80233ed560cc976e83570d0655c3472f20171fb3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a78adc](https://github.com/Ombi-app/Ombi/commit/8a78adc9bb62f277f2b213dcb3847ed6d0089fcb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d04c60a](https://github.com/Ombi-app/Ombi/commit/d04c60aa5909b47ba6bffa6f66b03079cbd43521))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([92a785e](https://github.com/Ombi-app/Ombi/commit/92a785e736fa4b72a45270da2d0f4661df433078))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([634982d](https://github.com/Ombi-app/Ombi/commit/634982df2661cefab5ea9f5163fe04a005cc0171))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([b404baa](https://github.com/Ombi-app/Ombi/commit/b404baad6d0aeaa1561701e0db8db4e78613a364))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d14f11e](https://github.com/Ombi-app/Ombi/commit/d14f11e0eb20ab0a68e765ee77968b3b3e54e995))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7cf64f9](https://github.com/Ombi-app/Ombi/commit/7cf64f909d78908edaabeffb8a39a7d02e73fe7e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c9e1ec](https://github.com/Ombi-app/Ombi/commit/0c9e1ec090827080cc8f7393e5e91456ff37d691))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([3b0b730](https://github.com/Ombi-app/Ombi/commit/3b0b730cb02efe24f6d4026e5fdb20d37e495119))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6ed1a03](https://github.com/Ombi-app/Ombi/commit/6ed1a03b7ff4077f09ea9e13394b18b0d138f4c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([2941acd](https://github.com/Ombi-app/Ombi/commit/2941acd3b2ec74a5e6aeea275ab5a39d2653f37f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c075a1a](https://github.com/Ombi-app/Ombi/commit/c075a1a66784d975eaf60f2dfbbcbe048f2f63d7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76bd81c](https://github.com/Ombi-app/Ombi/commit/76bd81c3ca55a98c6ec944a838dc01294a6193a6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0d38275](https://github.com/Ombi-app/Ombi/commit/0d3827507e002bcf58f673e97ffcc3bd25dcf337))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5c99601](https://github.com/Ombi-app/Ombi/commit/5c99601b07aec1a65d0186a4c4327440811e64c6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01546a0](https://github.com/Ombi-app/Ombi/commit/01546a0f7f86379528b486463246ef9bdfb9033e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d7fea78](https://github.com/Ombi-app/Ombi/commit/d7fea7843aaaab7ddff8dc31ca6d2a9117471dcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([1a6b95d](https://github.com/Ombi-app/Ombi/commit/1a6b95d45c220310213b8d811272a63f0f6ff42b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([fa10174](https://github.com/Ombi-app/Ombi/commit/fa1017422c4efd4b0897871bd3c671151774d7c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c31e62](https://github.com/Ombi-app/Ombi/commit/0c31e628df376aac6d56ae67c7c705a9a4a7c080))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6399643](https://github.com/Ombi-app/Ombi/commit/63996437a02fe10ffae6822ffa15369bec0a6b36))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5826e2d](https://github.com/Ombi-app/Ombi/commit/5826e2d9a1c3f1210a87fa270dc0c81bac32944a))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d434514](https://github.com/Ombi-app/Ombi/commit/d43451405be489254d7cdc7755d5f516a1e495a5))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0b9596d](https://github.com/Ombi-app/Ombi/commit/0b9596d807178f5e071113ec0347868ec7f0960b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8c4c0b2](https://github.com/Ombi-app/Ombi/commit/8c4c0b262978c1303767af360d802c4b4c2b4d24))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([289ab77](https://github.com/Ombi-app/Ombi/commit/289ab77b0e04aae235b6f6cebc86e0a8d1f0cf2b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([30e3417](https://github.com/Ombi-app/Ombi/commit/30e3417285a4eed18d429d7776f0e74096e834c0))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6c0a5da](https://github.com/Ombi-app/Ombi/commit/6c0a5dadd4b8f37760252eb0fe7f88908f55506d))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d5bf969](https://github.com/Ombi-app/Ombi/commit/d5bf9692ce1fc0ccfe7beca6dd200c78be177bdc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a9e7ea](https://github.com/Ombi-app/Ombi/commit/8a9e7ea588aefbcd73ed82625887e3614e1703ea))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01047a3](https://github.com/Ombi-app/Ombi/commit/01047a3fd67153f3ff16f860d2c7b50213e8d9b2))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([698a23f](https://github.com/Ombi-app/Ombi/commit/698a23fb83f323cdd1dd57cb49803079d44214a7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([24eb842](https://github.com/Ombi-app/Ombi/commit/24eb842fc4424f7bcc3ec2949d7f5472492e96f6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([ac8b16a](https://github.com/Ombi-app/Ombi/commit/ac8b16a3051ad71dbd54a8973c7dd847b564a515))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f428ce6](https://github.com/Ombi-app/Ombi/commit/f428ce6a700c081437703839bc84d2f2b1138bcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([94b16df](https://github.com/Ombi-app/Ombi/commit/94b16dfe09bf1d2cd6286777d74eb5d4496abbbb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4881775](https://github.com/Ombi-app/Ombi/commit/4881775eda69a8f136ce0d8fbbf970e3d0406dc9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8297db9](https://github.com/Ombi-app/Ombi/commit/8297db91e85da308bde6fb09ad78347dee063630))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1152ab](https://github.com/Ombi-app/Ombi/commit/d1152ab7674243daa528c524c0cdc87d81ad49c9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([eb2788b](https://github.com/Ombi-app/Ombi/commit/eb2788b761b55c487a59a049427ca08f6c10e836))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([21a794c](https://github.com/Ombi-app/Ombi/commit/21a794cbc0a5fa735ca0347c8f7f1ac04a487fbc))
### Features
* **email-notifications:** Add a link to Ombi details page in email notifications ([#4517](https://github.com/Ombi-app/Ombi/issues/4517)) ([a3e97b3](https://github.com/Ombi-app/Ombi/commit/a3e97b31e2298d95e7deebd71268095b8ed5e9dc))
* **media-details:** Add Trakt to social icons ([#4522](https://github.com/Ombi-app/Ombi/issues/4522)) ([d6ae79c](https://github.com/Ombi-app/Ombi/commit/d6ae79ce9eddbd5b7b888ab1b9f7e342d9d9ff9e))
## [4.10.2](https://github.com/Ombi-app/Ombi/compare/v4.10.1...v4.10.2) (2022-01-22)
## [4.12.7](https://github.com/Ombi-app/Ombi/compare/v4.12.6...v4.12.7) (2022-02-23)
## [4.16.10](https://github.com/Ombi-app/Ombi/compare/v4.16.9...v4.16.10) (2022-04-13)
## [4.12.6](https://github.com/Ombi-app/Ombi/compare/v4.12.5...v4.12.6) (2022-02-22)
## [4.16.9](https://github.com/Ombi-app/Ombi/compare/v4.16.8...v4.16.9) (2022-04-13)
### Bug Fixes
* **emby/jellyfin:** :bug: Fixed another issue where we were not correctly displaying the correct status' for movies ([5c0556e](https://github.com/Ombi-app/Ombi/commit/5c0556e6f44b8997a611f3a4d8e9e4e05d08bd13))
* **mediaserver:** fixed some more issues in the media server sync and availability checks ([f3ea979](https://github.com/Ombi-app/Ombi/commit/f3ea979b8bd77842780ce8e6928b16237dd779cf))
* **plex-watchlist:** Only request the latest season when importing from the watchlist ([77a47ff](https://github.com/Ombi-app/Ombi/commit/77a47ff157c6c5feafe3f2a29a3fcba8df4fdfef))
## [4.12.5](https://github.com/Ombi-app/Ombi/compare/v4.12.4...v4.12.5) (2022-02-21)
## [4.16.8](https://github.com/Ombi-app/Ombi/compare/v4.16.7...v4.16.8) (2022-04-13)
### Bug Fixes
* **emby:** :bug: Fixed the emby content sync [#4513](https://github.com/Ombi-app/Ombi/issues/4513) ([2927504](https://github.com/Ombi-app/Ombi/commit/2927504f0e0b4e7251e69b44e0e30c7ec9519980))
* **emby:** :bug: Fixed the emby content sync [#4513](https://github.com/Ombi-app/Ombi/issues/4513) ([bd441cb](https://github.com/Ombi-app/Ombi/commit/bd441cb54fd77d6befb03fae321dc36c29f0de2e))
* **availability:** Fixed an issue where we wouldn't mark a available 4k movie as available (when 4K request feature is disabled) ([b492699](https://github.com/Ombi-app/Ombi/commit/b49269961d4830a530e3054976a47f519524948b))
## [4.12.4](https://github.com/Ombi-app/Ombi/compare/v4.12.3...v4.12.4) (2022-02-17)
## [4.16.7](https://github.com/Ombi-app/Ombi/compare/v4.16.6...v4.16.7) (2022-04-12)
## [4.12.3](https://github.com/Ombi-app/Ombi/compare/v4.12.2...v4.12.3) (2022-02-16)
## [4.16.6](https://github.com/Ombi-app/Ombi/compare/v4.16.5...v4.16.6) (2022-04-11)
## [4.12.2](https://github.com/Ombi-app/Ombi/compare/v4.12.1...v4.12.2) (2022-02-16)
## [4.16.5](https://github.com/Ombi-app/Ombi/compare/v4.16.4...v4.16.5) (2022-04-08)
### Bug Fixes
* **requests:** :bug: Fixed the approve 4k option on the requests list not working as expected ([c0189da](https://github.com/Ombi-app/Ombi/commit/c0189dad478ea375beda61ba3bee3f029a39b8e5))
* **watchlist:** actually fixed it this time... ([d962a32](https://github.com/Ombi-app/Ombi/commit/d962a3211eca29520662ddce962676e3aea17ec5))
## [4.16.4](https://github.com/Ombi-app/Ombi/compare/v4.16.3...v4.16.4) (2022-04-08)
## [4.12.1](https://github.com/Ombi-app/Ombi/compare/v4.12.0...v4.12.1) (2022-02-16)
## [4.16.3](https://github.com/Ombi-app/Ombi/compare/v4.16.2...v4.16.3) (2022-04-08)
### Bug Fixes
* **requests:** :bug: Fixed the issue where Approving a 4K Request wouldn't send it to the correct 4K radarr instance ([87cb990](https://github.com/Ombi-app/Ombi/commit/87cb9903db30e1dead25ee8c5ea34305eb084a03)), closes [#4509](https://github.com/Ombi-app/Ombi/issues/4509)
* **plex-watchlist:** :bug: Fixed the issue where the watchlist didn't work for users logging in via OAuth ([6398f6a](https://github.com/Ombi-app/Ombi/commit/6398f6a4f7755281ebeac537e3ff623df5cfa0f3))
# [4.12.0](https://github.com/Ombi-app/Ombi/compare/v4.11.8...v4.12.0) (2022-02-14)
## [4.16.2](https://github.com/Ombi-app/Ombi/compare/v4.16.1...v4.16.2) (2022-04-07)
### Features
### Bug Fixes
* **radarr:** 4K Requests and Radarr 4K support ([ba88848](https://github.com/Ombi-app/Ombi/commit/ba88848866b0a9dedb1e79b55c4d81a0fd453843))
* **wizard:** Fixed an issue when using Plex OAuth it could fail setting up ([b743cf4](https://github.com/Ombi-app/Ombi/commit/b743cf4fafa7341ad1b163276f006d7ab0e9dcff))
## [4.11.8](https://github.com/Ombi-app/Ombi/compare/v4.11.7...v4.11.8) (2022-02-13)
## [4.16.1](https://github.com/Ombi-app/Ombi/compare/v4.16.0...v4.16.1) (2022-04-07)
### Bug Fixes
* **settings:** :bug: Fixed an issue where we were not displaying the excluded keyworks correctly in the TheMovieDbSettings page ([d3b3316](https://github.com/Ombi-app/Ombi/commit/d3b3316cbac18356b2f6b0912a3deb2c183e6534))
# [4.16.0](https://github.com/Ombi-app/Ombi/compare/v4.15.6...v4.16.0) (2022-04-07)
## [4.11.7](https://github.com/Ombi-app/Ombi/compare/v4.11.6...v4.11.7) (2022-02-12)
## [4.15.6](https://github.com/Ombi-app/Ombi/compare/v4.15.5...v4.15.6) (2022-04-07)
### Bug Fixes
* **notifications:** :bug: This is a fix for some of the duplicate notification issues [#3825](https://github.com/Ombi-app/Ombi/issues/3825) ([22bb422](https://github.com/Ombi-app/Ombi/commit/22bb4226ead2d62e8c2c2c05be47d7da621402e2))
* **radarr:** Fixed an issue where we couldn't sync radarr content [#4577](https://github.com/Ombi-app/Ombi/issues/4577) ([a5355a3](https://github.com/Ombi-app/Ombi/commit/a5355a3023e6900c4dd1b0da4722d7596c03907f))
## [4.15.5](https://github.com/Ombi-app/Ombi/compare/v4.15.4...v4.15.5) (2022-04-06)
## [4.11.6](https://github.com/Ombi-app/Ombi/compare/v4.11.5...v4.11.6) (2022-02-10)
### Bug Fixes
## [4.15.4](https://github.com/Ombi-app/Ombi/compare/v4.15.3...v4.15.4) (2022-03-29)
* **plex:** Fixed an issue where in a rare case we couldn't sync the data [#4502](https://github.com/Ombi-app/Ombi/issues/4502) ([191318d](https://github.com/Ombi-app/Ombi/commit/191318ddad5a8148422955bf928f1c49b890e3eb))
## [4.15.3](https://github.com/Ombi-app/Ombi/compare/v4.15.2...v4.15.3) (2022-03-24)
## [4.11.5](https://github.com/Ombi-app/Ombi/compare/v4.11.4...v4.11.5) (2022-02-05)
## [4.15.2](https://github.com/Ombi-app/Ombi/compare/v4.15.1...v4.15.2) (2022-03-23)
### Bug Fixes
* **sonarr:** Fixed where requesting all seasons would only mark the latest as monitored [#4496](https://github.com/Ombi-app/Ombi/issues/4496) ([cfb85c2](https://github.com/Ombi-app/Ombi/commit/cfb85c23d77626b9ec1d99a6cf76497c438d0338))
* **metadata:** improved the metadata job to also lookup the media in Plex to see if it has any more uptodate metadata ([83d1a15](https://github.com/Ombi-app/Ombi/commit/83d1a15cc9d0ee91be73bd91c4672cf1bcf2728a))

@ -100,17 +100,17 @@ Here are some of the features Ombi has:
</a>
</td>
<td align="center">
<a href="https://github.com/anojht">
<img src="https://avatars.githubusercontent.com/u/21053678?v=4" width="50;" alt="anojht"/>
<a href="https://github.com/sephrat">
<img src="https://avatars.githubusercontent.com/u/34862846?v=4" width="50;" alt="sephrat"/>
<br />
<sub><b>Anojh Thayaparan</b></sub>
<sub><b>Sephrat</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/sephrat">
<img src="https://avatars.githubusercontent.com/u/34862846?v=4" width="50;" alt="sephrat"/>
<a href="https://github.com/anojht">
<img src="https://avatars.githubusercontent.com/u/21053678?v=4" width="50;" alt="anojht"/>
<br />
<sub><b>Sephrat</b></sub>
<sub><b>Anojh Thayaparan</b></sub>
</a>
</td></tr>
<tr>
@ -222,10 +222,10 @@ Here are some of the features Ombi has:
</a>
</td>
<td align="center">
<a href="https://github.com/steffokeffo">
<img src="https://avatars.githubusercontent.com/u/8499989?v=4" width="50;" alt="steffokeffo"/>
<a href="https://github.com/grimsan55">
<img src="https://avatars.githubusercontent.com/u/8499989?v=4" width="50;" alt="grimsan55"/>
<br />
<sub><b>Steffokeffo</b></sub>
<sub><b>Stefan</b></sub>
</a>
</td>
<td align="center">
@ -286,6 +286,13 @@ Here are some of the features Ombi has:
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/dr3am37">
<img src="https://avatars.githubusercontent.com/u/91037083?v=4" width="50;" alt="dr3am37"/>
<br />
<sub><b>Dr3amer</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/mhann">
<img src="https://avatars.githubusercontent.com/u/17162399?v=4" width="50;" alt="mhann"/>
@ -320,15 +327,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Austin Jackson</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/D34DC3N73R">
<img src="https://avatars.githubusercontent.com/u/9123670?v=4" width="50;" alt="D34DC3N73R"/>
<br />
<sub><b>D34DC3N73R</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/pooley182">
<img src="https://avatars.githubusercontent.com/u/5040011?v=4" width="50;" alt="pooley182"/>
@ -363,15 +370,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Jeffrey Peters</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/MariusSchiffer">
<img src="https://avatars.githubusercontent.com/u/183124?v=4" width="50;" alt="MariusSchiffer"/>
<br />
<sub><b>Marius Schiffer</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Qstick">
<img src="https://avatars.githubusercontent.com/u/376117?v=4" width="50;" alt="Qstick"/>
@ -406,15 +413,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Abe Kline</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/XanderStrike">
<img src="https://avatars.githubusercontent.com/u/1565303?v=4" width="50;" alt="XanderStrike"/>
<br />
<sub><b>Alexander Standke</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Alasano">
<img src="https://avatars.githubusercontent.com/u/14372930?v=4" width="50;" alt="Alasano"/>
@ -449,15 +456,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Chris Lees</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/cdemi">
<img src="https://avatars.githubusercontent.com/u/8025435?v=4" width="50;" alt="cdemi"/>
<br />
<sub><b>Christopher Demicoli</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Codehhh">
<img src="https://avatars.githubusercontent.com/u/12055335?v=4" width="50;" alt="Codehhh"/>
@ -492,15 +499,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Devin Buhl</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/elisspace">
<img src="https://avatars.githubusercontent.com/u/18365129?v=4" width="50;" alt="elisspace"/>
<br />
<sub><b>Eli</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Fish2">
<img src="https://avatars.githubusercontent.com/u/2311734?v=4" width="50;" alt="Fish2"/>
@ -515,6 +522,13 @@ Here are some of the features Ombi has:
<sub><b>Haries Ramdhani</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/comigor">
<img src="https://avatars.githubusercontent.com/u/735858?v=4" width="50;" alt="comigor"/>
<br />
<sub><b>Igor Borges</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ImgBotApp">
<img src="https://avatars.githubusercontent.com/u/31427850?v=4" width="50;" alt="ImgBotApp"/>
@ -528,7 +542,8 @@ Here are some of the features Ombi has:
<br />
<sub><b>Jacob Pyke</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/jamesmacwhite">
<img src="https://avatars.githubusercontent.com/u/8067792?v=4" width="50;" alt="jamesmacwhite"/>
@ -542,8 +557,7 @@ Here are some of the features Ombi has:
<br />
<sub><b>Joe Groocock</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/errorhandler">
<img src="https://avatars.githubusercontent.com/u/17112958?v=4" width="50;" alt="errorhandler"/>
@ -571,7 +585,8 @@ Here are some of the features Ombi has:
<br />
<sub><b>Kris Klosterman</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/kmlucy">
<img src="https://avatars.githubusercontent.com/u/13952475?v=4" width="50;" alt="kmlucy"/>
@ -585,8 +600,14 @@ Here are some of the features Ombi has:
<br />
<sub><b>Lightkeeper</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Lucane">
<img src="https://avatars.githubusercontent.com/u/7999446?v=4" width="50;" alt="Lucane"/>
<br />
<sub><b>Lucane</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/devbymadde">
<img src="https://avatars.githubusercontent.com/u/6094593?v=4" width="50;" alt="devbymadde"/>
@ -594,13 +615,21 @@ Here are some of the features Ombi has:
<sub><b>Madeleine Schönemann</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/marleypowell">
<img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/>
<br />
<sub><b>Marley</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/mattmattmatt">
<img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/>
<br />
<sub><b>Matt</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/LMaxence">
<img src="https://avatars.githubusercontent.com/u/29194680?v=4" width="50;" alt="LMaxence"/>
@ -628,8 +657,7 @@ Here are some of the features Ombi has:
<br />
<sub><b>Qiming Chen</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/randallbruder">
<img src="https://avatars.githubusercontent.com/u/6447487?v=4" width="50;" alt="randallbruder"/>
@ -643,7 +671,8 @@ Here are some of the features Ombi has:
<br />
<sub><b>Rob Gökemeijer</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/sambartik">
<img src="https://avatars.githubusercontent.com/u/63553146?v=4" width="50;" alt="sambartik"/>
@ -665,21 +694,28 @@ Here are some of the features Ombi has:
<sub><b>Shoghi</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Teifun2">
<img src="https://avatars.githubusercontent.com/u/7461832?v=4" width="50;" alt="Teifun2"/>
<br />
<sub><b>Teifun2</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/thomasvt1">
<img src="https://avatars.githubusercontent.com/u/2271011?v=4" width="50;" alt="thomasvt1"/>
<br />
<sub><b>Thomas Van Tilburg</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Tim-Trott">
<img src="https://avatars.githubusercontent.com/u/8249434?v=4" width="50;" alt="Tim-Trott"/>
<br />
<sub><b>Tim Trott</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/tombomb">
<img src="https://avatars.githubusercontent.com/u/544509?v=4" width="50;" alt="tombomb"/>
@ -714,15 +750,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Tim OBrien</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/x-limitless-x">
<img src="https://avatars.githubusercontent.com/u/17127926?v=4" width="50;" alt="x-limitless-x"/>
<br />
<sub><b>Blake Drumm</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/camjac251">
<img src="https://avatars.githubusercontent.com/u/6313132?v=4" width="50;" alt="camjac251"/>
@ -744,6 +780,13 @@ Here are some of the features Ombi has:
<sub><b>Dorian ALKOUM</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/echel0n">
<img src="https://avatars.githubusercontent.com/u/1128022?v=4" width="50;" alt="echel0n"/>
<br />
<sub><b>Echel0n</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/m4tta">
<img src="https://avatars.githubusercontent.com/u/427218?v=4" width="50;" alt="m4tta"/>
@ -766,6 +809,13 @@ Here are some of the features Ombi has:
<sub><b>Patrick Weber</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/mkgeeky">
<img src="https://avatars.githubusercontent.com/u/68811367?v=4" width="50;" alt="mkgeeky"/>
<br />
<sub><b>Mkgeeky</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/sir-marv">
<img src="https://avatars.githubusercontent.com/u/3598205?v=4" width="50;" alt="sir-marv"/>

@ -0,0 +1,285 @@
**/bin/
**/obj/
**/.angular/
**/node_modules/
.gitignore
.git/
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/
tools/Cake.CoreCLR
.vscode
tools
.dotnet
Dockerfile
# .env file contains default environment variables for docker
.env
.git/

@ -106,7 +106,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("Fields", "ProviderIds,Overview");
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("IsMissing", "False");
return await Api.Request<EmbyItemContainer<EmbyMovie>>(request);
}
@ -180,7 +180,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("IsMissing", "False");
AddHeaders(request, apiKey);
@ -207,7 +207,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("IncludeItemTypes", type);
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("IsMissing", "False");
AddHeaders(request, apiKey);
@ -229,7 +229,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("isMissing", "False");
AddHeaders(request, apiKey);

@ -82,7 +82,7 @@ namespace Ombi.Api.Jellyfin
request.AddQueryString("Fields", "ProviderIds,Overview");
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("isMissing", "False");
return await Api.Request<JellyfinItemContainer<JellyfinMovie>>(request);
}
@ -143,7 +143,7 @@ namespace Ombi.Api.Jellyfin
request.AddQueryString("IncludeItemTypes", type);
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("isMissing", "False");
AddHeaders(request, apiKey);
@ -165,7 +165,7 @@ namespace Ombi.Api.Jellyfin
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False");
request.AddQueryString("isMissing", "False");
AddHeaders(request, apiKey);

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Ombi.Api.Plex.Models
{

@ -9,7 +9,7 @@
public class SickRageEpisodeSetStatus
{
public Data data { get; set; }
public Data[] data { get; set; }
public string message { get; set; }
public string result { get; set; }
}

@ -19,7 +19,7 @@ namespace Ombi.Api.Sonarr
protected IApi Api { get; }
protected virtual string ApiBaseUrl => "/api/";
public async Task<IEnumerable<SonarrProfile>> GetProfiles(string apiKey, string baseUrl)
public virtual async Task<IEnumerable<SonarrProfile>> GetProfiles(string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}profile", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);

@ -1,6 +1,8 @@
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models;
using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr
@ -21,5 +23,12 @@ namespace Ombi.Api.Sonarr
return await Api.Request<List<LanguageProfiles>>(request);
}
public override async Task<IEnumerable<SonarrProfile>> GetProfiles(string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}qualityprofile", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrProfile>>(request);
}
}
}

@ -136,6 +136,7 @@ namespace Ombi.Core.Engine
Status = movieInfo.Status,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.Now,
Approved = false,
Approved4K = false,
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode,
@ -163,7 +164,7 @@ namespace Ombi.Core.Engine
};
}
if (requestModel.Approved) // The rules have auto approved this
if (requestModel.Approved || requestModel.Approved4K) // The rules have auto approved this
{
var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf, isExisting, is4kRequest);
if (requestEngineResult.Result)
@ -333,7 +334,7 @@ namespace Ombi.Core.Engine
allRequests = allRequests.Where(x =>
(x.RequestedDate != DateTime.MinValue && x.Approved && !x.Available && (!x.Denied.HasValue || !x.Denied.Value))
||
(x.Has4KRequest && x.Approved4K && !x.Available && (!x.Denied.HasValue || !x.Denied.Value))
(x.Has4KRequest && x.Approved4K && !x.Available4K && (!x.Denied4K.HasValue || !x.Denied4K.Value))
);
break;
case RequestStatus.Available:
@ -532,8 +533,11 @@ namespace Ombi.Core.Engine
x.ShowSubscribe = false;
}
else
{
if (!x.Available && !x.Available4K && (!x.Denied ?? true) && (!x.Denied4K ?? true))
{
x.ShowSubscribe = true;
}
var hasSub = sub.FirstOrDefault(r => r.RequestId == x.Id);
x.Subscribed = hasSub != null;
}

@ -16,7 +16,6 @@ using Ombi.Store.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
namespace Ombi.Core.Engine
@ -161,7 +160,7 @@ namespace Ombi.Core.Engine
var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode);
return await MovieApi.UpcomingMovies(langCode);
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
@ -216,34 +215,9 @@ namespace Ombi.Core.Engine
await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
return viewMovie;
}
private async Task CheckForSubscription(SearchMovieViewModel viewModel)
{
// Check if this user requested it
var user = await GetUser();
if (user == null)
{
return;
}
var request = await RequestService.MovieRequestService.GetAll()
.AnyAsync(x => x.RequestedUserId.Equals(user.Id) && x.TheMovieDbId == viewModel.Id);
if (request || viewModel.Available)
{
viewModel.ShowSubscribe = false;
}
else
{
viewModel.ShowSubscribe = true;
var sub = await _subscriptionRepository.GetAll().FirstOrDefaultAsync(s => s.UserId == user.Id
&& s.RequestId == viewModel.RequestId && s.RequestType == RequestType.Movie);
viewModel.Subscribed = sub != null;
}
}
private async Task<SearchMovieViewModel> ProcessSingleMovie(MovieDbSearchResult movie)
{

@ -269,8 +269,11 @@ namespace Ombi.Core.Engine
x.ShowSubscribe = false;
}
else
{
if (!x.Available && (!x.Denied ?? false))
{
x.ShowSubscribe = true;
}
var hasSub = sub.FirstOrDefault(r => r.RequestId == x.Id);
x.Subscribed = hasSub != null;
}

@ -885,8 +885,11 @@ namespace Ombi.Core.Engine
x.ShowSubscribe = false;
}
else
{
if (!x.Available && (!x.Denied ?? true))
{
x.ShowSubscribe = true;
}
var result = relevantSubs.FirstOrDefault(s => s.RequestId == x.Id);
x.Subscribed = result != null;
}

@ -106,11 +106,17 @@ namespace Ombi.Core.Engine
SeasonNumber = e.season,
Episodes = new List<EpisodeRequests>()
};
var hasAirDate = e.airstamp.HasValue();
var airDate = DateTime.MinValue;
if (hasAirDate)
{
hasAirDate = DateTime.TryParse(e.airdate, out airDate);
}
newSeason.Episodes.Add(new EpisodeRequests
{
Url = e.url.ToHttpsUrl(),
Title = e.name,
AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue,
AirDate = airDate,
EpisodeNumber = e.number,
});
@ -118,12 +124,18 @@ namespace Ombi.Core.Engine
}
else
{
var hasAirDate = e.airstamp.HasValue();
var airDate = DateTime.MinValue;
if (hasAirDate)
{
hasAirDate = DateTime.TryParse(e.airdate, out airDate);
}
// We already have the season, so just add the episode
season.Episodes.Add(new EpisodeRequests
{
Url = e.url.ToHttpsUrl(),
Title = e.name,
AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue,
AirDate = airDate,
EpisodeNumber = e.number,
});
}

@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Models.Search.V2;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
@ -14,5 +15,6 @@ namespace Ombi.Core.Engine.V2
{
Task<List<MultiSearchResult>> MultiSearch(string searchTerm, MultiSearchFilter filter, CancellationToken cancellationToken);
Task<IEnumerable<Genre>> GetGenres(string media, CancellationToken requestAborted);
Task<IEnumerable<Language>> GetLanguages(CancellationToken requestAborted);
}
}

@ -11,6 +11,7 @@ using Ombi.Core.Models.Search;
using Ombi.Core.Models.Search.V2;
using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Services;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
@ -31,7 +32,8 @@ namespace Ombi.Core.Engine.V2
{
public MovieSearchEngineV2(ICurrentUser identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngineV2> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s, IRepository<RequestSubscription> sub,
ISettingsService<CustomizationSettings> customizationSettings, IMovieRequestEngine movieRequestEngine, IHttpClientFactory httpClientFactory)
ISettingsService<CustomizationSettings> customizationSettings, IMovieRequestEngine movieRequestEngine, IHttpClientFactory httpClientFactory,
IFeatureService feature)
: base(identity, service, r, um, mem, s, sub)
{
MovieApi = movApi;
@ -40,6 +42,7 @@ namespace Ombi.Core.Engine.V2
_customizationSettings = customizationSettings;
_movieRequestEngine = movieRequestEngine;
_client = httpClientFactory.CreateClient();
_feature = feature;
}
private IMovieDbApi MovieApi { get; }
@ -48,6 +51,7 @@ namespace Ombi.Core.Engine.V2
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly IMovieRequestEngine _movieRequestEngine;
private readonly HttpClient _client;
private readonly IFeatureService _feature;
public async Task<MovieFullInfoViewModel> GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null)
{
@ -148,15 +152,15 @@ namespace Ombi.Core.Engine.V2
{
var langCode = await DefaultLanguageCode(null);
//var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems);
var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems);
var results = new List<MovieDbSearchResult>();
//foreach (var pagesToLoad in pages)
//{
var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken);
//results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
//}
return await TransformMovieResultsToResponse(apiResult);
foreach (var pagesToLoad in pages)
{
var apiResult = await MovieApi.AdvancedSearch(model, pagesToLoad.Page, cancellationToken);
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
}
/// <summary>
@ -196,14 +200,19 @@ namespace Ombi.Core.Engine.V2
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies(int currentPosition, int amountToLoad)
{
var langCode = await DefaultLanguageCode(null);
var isOldTrendingSourceEnabled = await _feature.FeatureEnabled(FeatureNames.OldTrendingSource);
var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems);
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var search = () => (isOldTrendingSourceEnabled) ?
MovieApi.NowPlaying(langCode, pagesToLoad.Page)
: MovieApi.TrendingMovies(langCode, pagesToLoad.Page);
var apiResult = await Cache.GetOrAddAsync(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode,
() => MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
search, DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -278,7 +287,7 @@ namespace Ombi.Core.Engine.V2
var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode);
return await MovieApi.UpcomingMovies(langCode);
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
@ -298,7 +307,7 @@ namespace Ombi.Core.Engine.V2
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAddAsync(nameof(UpcomingMovies) + pagesToLoad.Page + langCode,
() => MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
() => MovieApi.UpcomingMovies(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -393,8 +402,6 @@ namespace Ombi.Core.Engine.V2
await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
var mapped = Mapper.Map<MovieFullInfoViewModel>(movie);
mapped.Available = viewMovie.Available;
@ -406,8 +413,6 @@ namespace Ombi.Core.Engine.V2
mapped.PlexUrl = viewMovie.PlexUrl;
mapped.EmbyUrl = viewMovie.EmbyUrl;
mapped.JellyfinUrl = viewMovie.JellyfinUrl;
mapped.Subscribed = viewMovie.Subscribed;
mapped.ShowSubscribe = viewMovie.ShowSubscribe;
mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate;
mapped.RequestedDate4k = viewMovie.RequestedDate4k;
mapped.Approved4K = viewMovie.Approved4K;
@ -429,8 +434,6 @@ namespace Ombi.Core.Engine.V2
var mappedMovie = Mapper.Map<SearchMovieViewModel>(movie);
await RunSearchRules(mappedMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(mappedMovie);
var mapped = Mapper.Map<MovieCollection>(movie);
mapped.Available = movie.Available;
@ -440,8 +443,6 @@ namespace Ombi.Core.Engine.V2
mapped.PlexUrl = movie.PlexUrl;
mapped.EmbyUrl = movie.EmbyUrl;
mapped.JellyfinUrl = movie.JellyfinUrl;
mapped.Subscribed = movie.Subscribed;
mapped.ShowSubscribe = movie.ShowSubscribe;
mapped.ReleaseDate = movie.ReleaseDate;
}
return viewMovie;
@ -470,34 +471,9 @@ namespace Ombi.Core.Engine.V2
await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
return viewMovie;
}
private async Task CheckForSubscription(SearchViewModel viewModel)
{
// Check if this user requested it
var user = await GetUser();
if (user == null)
{
return;
}
var request = await RequestService.MovieRequestService.GetAll()
.AnyAsync(x => x.RequestedUserId.Equals(user.Id) && x.TheMovieDbId == viewModel.Id);
if (request)
{
viewModel.ShowSubscribe = false;
}
else
{
viewModel.ShowSubscribe = true;
var sub = await _subscriptionRepository.GetAll().FirstOrDefaultAsync(s => s.UserId == user.Id
&& s.RequestId == viewModel.RequestId && s.RequestType == RequestType.Movie);
viewModel.Subscribed = sub != null;
}
}
public async Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken cancellationToken)
{

@ -17,6 +17,7 @@ using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
@ -124,5 +125,9 @@ namespace Ombi.Core.Engine.V2
var lang = await DefaultLanguageCode(null);
return await _movieDbApi.GetGenres(media, cancellationToken, lang);
}
public async Task<IEnumerable<Language>> GetLanguages(CancellationToken cancellationToken)
{
return await _movieDbApi.GetLanguages(cancellationToken);
}
}
}

@ -26,6 +26,7 @@ using System.Diagnostics;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.UI;
using Ombi.Core.Helpers;
using Ombi.Core.Services;
namespace Ombi.Core.Engine.V2
{
@ -37,10 +38,12 @@ namespace Ombi.Core.Engine.V2
private readonly IMovieDbApi _movieApi;
private readonly ISettingsService<CustomizationSettings> _customization;
private readonly ITvRequestEngine _requestEngine;
private readonly IFeatureService _feature;
public TvSearchEngineV2(ICurrentUser identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s,
IRepository<RequestSubscription> sub, IMovieDbApi movieApi, ISettingsService<CustomizationSettings> customization, ITvRequestEngine requestEngine)
IRepository<RequestSubscription> sub, IMovieDbApi movieApi, ISettingsService<CustomizationSettings> customization, ITvRequestEngine requestEngine,
IFeatureService feature)
: base(identity, service, r, um, memCache, s, sub)
{
_tvMaze = tvMaze;
@ -49,6 +52,7 @@ namespace Ombi.Core.Engine.V2
_movieApi = movieApi;
_customization = customization;
_requestEngine = requestEngine;
_feature = feature;
}
@ -134,13 +138,17 @@ namespace Ombi.Core.Engine.V2
public async Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad)
{
var langCode = await DefaultLanguageCode(null);
var isOldTrendingSourceEnabled = await _feature.FeatureEnabled(FeatureNames.OldTrendingSource);
var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit);
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var search = ( async () => (isOldTrendingSourceEnabled) ?
await _movieApi.TopRatedTv(langCode, pagesToLoad.Page)
: await _movieApi.TrendingTv(langCode, pagesToLoad.Page));
var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
search, DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
@ -264,11 +272,17 @@ namespace Ombi.Core.Engine.V2
Overview = tvSeason.overview,
Episodes = new List<EpisodeRequests>()
};
var hasAirDate = episode.air_date.HasValue();
var airDate = DateTime.MinValue;
if (hasAirDate)
{
DateTime.TryParse(episode.air_date, out airDate);
}
newSeason.Episodes.Add(new EpisodeRequests
{
//Url = episode...ToHttpsUrl(),
Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue,
AirDate = airDate,
EpisodeNumber = episode.episode_number,
});
@ -276,12 +290,18 @@ namespace Ombi.Core.Engine.V2
}
else
{
var hasAirDate = episode.air_date.HasValue();
var airDate = DateTime.MinValue;
if (hasAirDate)
{
DateTime.TryParse(episode.air_date, out airDate);
}
// We already have the season, so just add the episode
season.Episodes.Add(new EpisodeRequests
{
//Url = e.url.ToHttpsUrl(),
Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue,
AirDate = airDate,
EpisodeNumber = episode.episode_number,
});
}

@ -261,9 +261,16 @@ namespace Ombi.Core.Helpers
return this;
}
private DateTime FormatDate(string date)
private static DateTime FormatDate(string date)
{
return string.IsNullOrEmpty(date) ? DateTime.MinValue : DateTime.Parse(date);
if (date.HasValue())
{
if (DateTime.TryParse(date, out var d))
{
return d;
}
}
return DateTime.MinValue;
}
}
}

@ -9,6 +9,7 @@ using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using System.Threading;
using Ombi.Helpers;
namespace Ombi.Core.Helpers
{
@ -255,9 +256,16 @@ namespace Ombi.Core.Helpers
return this;
}
private DateTime FormatDate(string date)
private static DateTime FormatDate(string date)
{
return string.IsNullOrEmpty(date) ? DateTime.MinValue : DateTime.Parse(date);
if (date.HasValue())
{
if (DateTime.TryParse(date, out var d))
{
return d;
}
}
return DateTime.MinValue;
}
}
}

@ -36,6 +36,7 @@ namespace Ombi.Core.Models.Search
[Obsolete("Use request service instead")]
public bool Subscribed { get; set; }
[NotMapped]
[Obsolete("Use request service instead")]
public bool ShowSubscribe { get; set; }
}
}

@ -70,7 +70,7 @@ namespace Ombi.Core.Senders
}
return new SenderResult { Success = false, Sent = false, Message = "Something went wrong!" };
return new SenderResult { Success = false, Sent = false };
}
private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)

@ -133,8 +133,7 @@ namespace Ombi.Core.Senders
return new SenderResult
{
Success = false,
Message = "Something went wrong!"
Success = false
};
}
@ -343,8 +342,6 @@ namespace Ombi.Core.Senders
await Task.Delay(500);
}
var seriesChanges = false;
foreach (var season in model.SeasonRequests)
{
foreach (var ep in season.Episodes)
@ -359,53 +356,22 @@ namespace Ombi.Core.Senders
}
existingSeason = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpisodeList = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber).ToList();
var sonarrEpCount = sonarrEpisodeList.Count;
var ourRequestCount = season.Episodes.Count;
var ourEpisodes = season.Episodes.Select(x => x.EpisodeNumber).ToList();
var unairedEpisodes = sonarrEpisodeList.Where(x => x.airDateUtc > DateTime.UtcNow).Select(x => x.episodeNumber).ToList();
//// Check if we have requested all the latest episodes, if we have then monitor
//// NOTE, not sure if needed since ombi ui displays future episodes anyway...
//ourEpisodes.AddRange(unairedEpisodes);
//var distinctEpisodes = ourEpisodes.Distinct().ToList();
//var missingEpisodes = Enumerable.Range(distinctEpisodes.Min(), distinctEpisodes.Count).Except(distinctEpisodes);
if (sonarrEpCount == ourRequestCount /*|| !missingEpisodes.Any()*/)
{
// We have the same amount of requests as all of the episodes in the season.
existingSeason.monitored = true;
seriesChanges = true;
// We do not need to update the episodes as marking the season as monitored will mark the episodes as monitored.
var seasonToUpdate = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber);
seasonToUpdate.monitored = true; // Update by ref
}
else
{
// Make sure this season is set to monitored
if (!existingSeason.monitored)
{
// We need to monitor it, problem being is all episodes will now be monitored
// So we need to monitor the series but unmonitor every episode
// Except the episodes that are already monitored before we update the series (we do not want to unmonitored episodes that are monitored beforehand)
existingSeason.monitored = true;
var sea = result.seasons.FirstOrDefault(x => x.seasonNumber == existingSeason.seasonNumber);
sea.monitored = true;
//var previouslyMonitoredEpisodes = sonarrEpList.Where(x =>
// x.seasonNumber == existingSeason.seasonNumber && x.monitored).Select(x => x.episodeNumber).ToList(); // We probably don't actually care about this
result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
var epToUnmonitored = new List<Episode>();
var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the original member
foreach (var ep in newEpList.Where(x => x.seasonNumber == existingSeason.seasonNumber).ToList())
{
//if (previouslyMonitoredEpisodes.Contains(ep.episodeNumber))
//{
// // This was previously monitored.
// continue;
//}
ep.monitored = false;
epToUnmonitored.Add(ep);
}
@ -421,11 +387,6 @@ namespace Ombi.Core.Senders
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
}
}
if (seriesChanges)
{
await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result);
}
if (!s.AddOnly)
{

@ -83,6 +83,8 @@ namespace Ombi.Helpers.Tests
.SetName("PaginationPosition_Load_SecondHalf_FirstPage_FirstHalf_SecondPage");
yield return new TestCaseData(0, 40, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(1, 20, 0), new MultiplePagesTestData(2, 20, 0) })
.SetName("PaginationPosition_Load_Full_First_And_SecondPage");
yield return new TestCaseData(40, 40, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(3, 20, 0), new MultiplePagesTestData(4, 20, 0) })
.SetName("PaginationPosition_Load_Full_Third_And_ForthPage");
yield return new TestCaseData(35, 15, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(2, 5, 15), new MultiplePagesTestData(3, 10, 0) })
.SetName("PaginationPosition_Load_EndSecondPage_Beginning_ThirdPage");
yield return new TestCaseData(18, 22, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(1, 2, 18), new MultiplePagesTestData(2, 20, 0) })

@ -17,7 +17,7 @@ namespace Ombi.Helpers
var lastPage = lastItemIndex / maxItemsPerPage + 1;
var stopPos = lastItemIndex % maxItemsPerPage + 1;
while (currentlyLoaded > maxItemsPerPage)
while (currentlyLoaded >= maxItemsPerPage)
{
currentlyLoaded -= maxItemsPerPage;
}

@ -139,7 +139,7 @@
<value>Episoden:</value>
</data>
<data name="PoweredBy" xml:space="preserve">
<value>Powered by</value>
<value>Betrieben durch</value>
</data>
<data name="Unsubscribe" xml:space="preserve">
<value>Abbestellen</value>
@ -148,9 +148,9 @@
<value>Album</value>
</data>
<data name="Movie" xml:space="preserve">
<value>Movie</value>
<value>Film</value>
</data>
<data name="TvShow" xml:space="preserve">
<value>TV Show</value>
<value>TV-Serie</value>
</data>
</root>

@ -118,13 +118,13 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NewAlbums" xml:space="preserve">
<value>Nowy Album</value>
<value>Nowe Albumy</value>
</data>
<data name="NewMovies" xml:space="preserve">
<value>Nowy film</value>
<value>Nowe filmy</value>
</data>
<data name="NewTV" xml:space="preserve">
<value>Nowy telewizor</value>
<value>Nowe seriale</value>
</data>
<data name="GenresLabel" xml:space="preserve">
<value>Gatunki:</value>
@ -136,21 +136,21 @@
<value>Sezon:</value>
</data>
<data name="EpisodesLabel" xml:space="preserve">
<value>Odcinków:</value>
<value>Odcinki:</value>
</data>
<data name="PoweredBy" xml:space="preserve">
<value>Wspierane przez</value>
</data>
<data name="Unsubscribe" xml:space="preserve">
<value>Wypis się</value>
<value>Wypisz się</value>
</data>
<data name="Album" xml:space="preserve">
<value>Album</value>
</data>
<data name="Movie" xml:space="preserve">
<value>Movie</value>
<value>Film</value>
</data>
<data name="TvShow" xml:space="preserve">
<value>TV Show</value>
<value>Serial</value>
</data>
</root>

@ -7,10 +7,12 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core;
using Ombi.Core.Notifications;
using Ombi.Core.Services;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
@ -21,7 +23,7 @@ namespace Ombi.Schedule.Jobs.Emby
public class EmbyAvaliabilityChecker : IEmbyAvaliabilityChecker
{
public EmbyAvaliabilityChecker(IEmbyContentRepository repo, ITvRequestRepository t, IMovieRequestRepository m,
INotificationHelper n, ILogger<EmbyAvaliabilityChecker> log, IHubContext<NotificationHub> notification)
INotificationHelper n, ILogger<EmbyAvaliabilityChecker> log, IHubContext<NotificationHub> notification, IFeatureService featureService)
{
_repo = repo;
_tvRepo = t;
@ -29,6 +31,7 @@ namespace Ombi.Schedule.Jobs.Emby
_notificationService = n;
_log = log;
_notification = notification;
_featureService = featureService;
}
private readonly ITvRequestRepository _tvRepo;
@ -37,6 +40,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly INotificationHelper _notificationService;
private readonly ILogger<EmbyAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _notification;
private readonly IFeatureService _featureService;
public async Task Execute(IJobExecutionContext job)
{
@ -53,6 +57,7 @@ namespace Ombi.Schedule.Jobs.Emby
private async Task ProcessMovies()
{
var feature4kEnabled = await _featureService.FeatureEnabled(FeatureNames.Movie4KRequests);
var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available || (!x.Available4K && x.Has4KRequest));
foreach (var movie in movies)
@ -85,8 +90,8 @@ namespace Ombi.Schedule.Jobs.Emby
notify = true;
}
// If we have a non-4k versison then mark as available
if (embyContent.Quality != null && !movie.Available)
// If we have a non-4k version or we don't care about versions, then mark as available
if (!movie.Available && ( !feature4kEnabled || embyContent.Quality != null ))
{
movie.Available = true;
movie.MarkedAsAvailable = DateTime.Now;

@ -157,9 +157,20 @@ namespace Ombi.Schedule.Jobs.Emby
}
var existingTv = await _repo.GetByEmbyId(tvShow.Id);
if (existingTv != null &&
( existingTv.ImdbId != tvShow.ProviderIds?.Imdb
|| existingTv.TheMovieDbId != tvShow.ProviderIds?.Tmdb
|| existingTv.TvDbId != tvShow.ProviderIds?.Tvdb))
{
_logger.LogDebug($"Series '{tvShow.Name}' has different IDs, probably a reidentification.");
await _repo.DeleteTv(existingTv);
existingTv = null;
}
if (existingTv == null)
{
_logger.LogDebug("Adding new TV Show {0}", tvShow.Name);
_logger.LogDebug("Adding TV Show {0}", tvShow.Name);
mediaToAdd.Add(new EmbyContent
{
TvDbId = tvShow.ProviderIds?.Tvdb,
@ -265,23 +276,21 @@ namespace Ombi.Schedule.Jobs.Emby
return;
}
_logger.LogDebug($"Adding new movie {movieInfo.Name}");
content.Add(new EmbyContent
{
ImdbId = movieInfo.ProviderIds.Imdb,
TheMovieDbId = movieInfo.ProviderIds?.Tmdb,
Title = movieInfo.Name,
Type = MediaType.Movie,
EmbyId = movieInfo.Id,
Url = EmbyHelper.GetEmbyMediaUrl(movieInfo.Id, server?.ServerId, server.ServerHostname),
AddedAt = DateTime.UtcNow,
Quality = has4K ? null : quality,
Has4K = has4K
});
var newMovie = new EmbyContent();
newMovie.AddedAt = DateTime.UtcNow;
MapEmbyContent(newMovie, movieInfo, server, has4K, quality);
content.Add(newMovie);
}
else
{
if (!quality.Equals(existingMovie?.Quality, StringComparison.InvariantCultureIgnoreCase))
var movieHasChanged = false;
if (existingMovie.ImdbId != movieInfo.ProviderIds.Imdb || existingMovie.TheMovieDbId != movieInfo.ProviderIds.Tmdb)
{
_logger.LogDebug($"Updating existing movie '{movieInfo.Name}'");
MapEmbyContent(existingMovie, movieInfo, server, has4K, quality);
movieHasChanged = true;
}
else if (!quality.Equals(existingMovie?.Quality, StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogDebug($"We have found another quality for Movie '{movieInfo.Name}', Quality: '{quality}'");
existingMovie.Quality = has4K ? null : quality;
@ -290,6 +299,11 @@ namespace Ombi.Schedule.Jobs.Emby
// Probably could refactor here
// If a 4k movie comes in (we don't store the quality on 4k)
// it will always get updated even know it's not changed
movieHasChanged = true;
}
if (movieHasChanged)
{
toUpdate.Add(existingMovie);
}
else
@ -300,6 +314,17 @@ namespace Ombi.Schedule.Jobs.Emby
}
}
private void MapEmbyContent(EmbyContent content, EmbyMovie movieInfo, EmbyServers server, bool has4K, string quality){
content.ImdbId = movieInfo.ProviderIds.Imdb;
content.TheMovieDbId = movieInfo.ProviderIds?.Tmdb;
content.Title = movieInfo.Name;
content.Type = MediaType.Movie;
content.EmbyId = movieInfo.Id;
content.Url = EmbyHelper.GetEmbyMediaUrl(movieInfo.Id, server?.ServerId, server.ServerHostname);
content.Quality = has4K ? null : quality;
content.Has4K = has4K;
}
private bool ValidateSettings(EmbyServers server)
{
if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey))

@ -130,12 +130,6 @@ namespace Ombi.Schedule.Jobs.Emby
{
processed++;
if (ep.LocationType?.Equals("Virtual", StringComparison.InvariantCultureIgnoreCase) ?? false)
{
// For some reason Emby is not respecting the `IsVirtualItem` field.
continue;
}
// Let's make sure we have the parent request, stop those pesky forign key errors,
// Damn me having data integrity
var parent = await _repo.GetByEmbyId(ep.SeriesId);
@ -176,10 +170,15 @@ namespace Ombi.Schedule.Jobs.Emby
if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber)
{
int episodeNumber = ep.IndexNumber;
do
{
_logger.LogDebug($"Multiple-episode file detected. Adding episode ${episodeNumber}");
episodeNumber++;
epToAdd.Add(new EmbyEpisode
{
EmbyId = ep.Id,
EpisodeNumber = ep.IndexNumberEnd.Value,
EpisodeNumber = episodeNumber,
SeasonNumber = ep.ParentIndexNumber,
ParentId = ep.SeriesId,
TvDbId = ep.ProviderIds.Tvdb,
@ -188,6 +187,9 @@ namespace Ombi.Schedule.Jobs.Emby
Title = ep.Name,
AddedAt = DateTime.UtcNow
});
} while (episodeNumber < ep.IndexNumberEnd.Value);
}
}
}

@ -33,9 +33,11 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core;
using Ombi.Core.Services;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
@ -46,7 +48,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
public class JellyfinAvaliabilityChecker : IJellyfinAvaliabilityChecker
{
public JellyfinAvaliabilityChecker(IJellyfinContentRepository repo, ITvRequestRepository t, IMovieRequestRepository m,
INotificationHelper n, ILogger<JellyfinAvaliabilityChecker> log, IHubContext<NotificationHub> notification)
INotificationHelper n, ILogger<JellyfinAvaliabilityChecker> log, IHubContext<NotificationHub> notification, IFeatureService featureService)
{
_repo = repo;
_tvRepo = t;
@ -54,6 +56,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
_notificationService = n;
_log = log;
_notification = notification;
_featureService = featureService;
}
private readonly ITvRequestRepository _tvRepo;
@ -62,6 +65,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private readonly INotificationHelper _notificationService;
private readonly ILogger<JellyfinAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _notification;
private readonly IFeatureService _featureService;
public async Task Execute(IJobExecutionContext job)
{
@ -78,6 +82,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private async Task ProcessMovies()
{
var feature4kEnabled = await _featureService.FeatureEnabled(FeatureNames.Movie4KRequests);
var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available || (!x.Available4K && x.Has4KRequest));
foreach (var movie in movies)
@ -110,8 +115,8 @@ namespace Ombi.Schedule.Jobs.Jellyfin
notify = true;
}
// If we have a non-4k versison then mark as available
if (jellyfinContent.Quality != null && !movie.Available)
// If we have a non-4k version or we don't care about versions, then mark as available
if (!movie.Available && ( !feature4kEnabled || jellyfinContent.Quality != null ))
{
movie.Available = true;
movie.MarkedAsAvailable = DateTime.Now;

@ -132,9 +132,20 @@ namespace Ombi.Schedule.Jobs.Jellyfin
}
var existingTv = await _repo.GetByJellyfinId(tvShow.Id);
if (existingTv != null &&
( existingTv.ImdbId != tvShow.ProviderIds?.Imdb
|| existingTv.TheMovieDbId != tvShow.ProviderIds?.Tmdb
|| existingTv.TvDbId != tvShow.ProviderIds?.Tvdb))
{
_logger.LogDebug($"Series '{tvShow.Name}' has different IDs, probably a reidentification.");
await _repo.DeleteTv(existingTv);
existingTv = null;
}
if (existingTv == null)
{
_logger.LogDebug("Adding new TV Show {0}", tvShow.Name);
_logger.LogDebug("Adding TV Show {0}", tvShow.Name);
mediaToAdd.Add(new JellyfinContent
{
TvDbId = tvShow.ProviderIds?.Tvdb,
@ -230,22 +241,21 @@ namespace Ombi.Schedule.Jobs.Jellyfin
return;
}
_logger.LogDebug($"Adding new movie {movieInfo.Name}");
content.Add(new JellyfinContent
{
ImdbId = movieInfo.ProviderIds.Imdb,
TheMovieDbId = movieInfo.ProviderIds?.Tmdb,
Title = movieInfo.Name,
Type = MediaType.Movie,
JellyfinId = movieInfo.Id,
Url = JellyfinHelper.GetJellyfinMediaUrl(movieInfo.Id, server?.ServerId, server.ServerHostname),
AddedAt = DateTime.UtcNow,
Quality = has4K ? null : quality,
Has4K = has4K
});
var newMovie = new JellyfinContent();
newMovie.AddedAt = DateTime.UtcNow;
MapJellyfinMovie(newMovie, movieInfo, server, has4K, quality);
content.Add(newMovie);;
}
else
{
if (!quality.Equals(existingMovie?.Quality, StringComparison.InvariantCultureIgnoreCase))
var movieHasChanged = false;
if (existingMovie.ImdbId != movieInfo.ProviderIds.Imdb || existingMovie.TheMovieDbId != movieInfo.ProviderIds.Tmdb)
{
_logger.LogDebug($"Updating existing movie '{movieInfo.Name}'");
MapJellyfinMovie(existingMovie, movieInfo, server, has4K, quality);
movieHasChanged = true;
}
else if (!quality.Equals(existingMovie?.Quality, StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogDebug($"We have found another quality for Movie '{movieInfo.Name}', Quality: '{quality}'");
existingMovie.Quality = has4K ? null : quality;
@ -255,6 +265,12 @@ namespace Ombi.Schedule.Jobs.Jellyfin
// If a 4k movie comes in (we don't store the quality on 4k)
// it will always get updated even know it's not changed
toUpdate.Add(existingMovie);
movieHasChanged = true;
}
if (movieHasChanged)
{
toUpdate.Add(existingMovie);
}
else
{
@ -264,6 +280,18 @@ namespace Ombi.Schedule.Jobs.Jellyfin
}
}
private void MapJellyfinMovie(JellyfinContent content, JellyfinMovie movieInfo, JellyfinServers server, bool has4K, string quality)
{
content.ImdbId = movieInfo.ProviderIds.Imdb;
content.TheMovieDbId = movieInfo.ProviderIds?.Tmdb;
content.Title = movieInfo.Name;
content.Type = MediaType.Movie;
content.JellyfinId = movieInfo.Id;
content.Url = JellyfinHelper.GetJellyfinMediaUrl(movieInfo.Id, server?.ServerId, server.ServerHostname);
content.Quality = has4K ? null : quality;
content.Has4K = has4K;
}
private bool ValidateSettings(JellyfinServers server)
{
if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey))

@ -106,12 +106,6 @@ namespace Ombi.Schedule.Jobs.Jellyfin
{
processed++;
if (ep.LocationType?.Equals("Virtual", StringComparison.InvariantCultureIgnoreCase) ?? false)
{
// For some reason Jellyfin is not respecting the `IsVirtualItem` field.
continue;
}
// Let's make sure we have the parent request, stop those pesky forign key errors,
// Damn me having data integrity
var parent = await _repo.GetByJellyfinId(ep.SeriesId);
@ -152,10 +146,15 @@ namespace Ombi.Schedule.Jobs.Jellyfin
if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber)
{
int episodeNumber = ep.IndexNumber;
do
{
_logger.LogDebug($"Multiple-episode file detected. Adding episode ${episodeNumber}");
episodeNumber++;
epToAdd.Add(new JellyfinEpisode
{
JellyfinId = ep.Id,
EpisodeNumber = ep.IndexNumberEnd.Value,
EpisodeNumber = episodeNumber,
SeasonNumber = ep.ParentIndexNumber,
ParentId = ep.SeriesId,
TvDbId = ep.ProviderIds.Tvdb,
@ -164,6 +163,8 @@ namespace Ombi.Schedule.Jobs.Jellyfin
Title = ep.Name,
AddedAt = DateTime.UtcNow
});
} while (episodeNumber < ep.IndexNumberEnd.Value);
}
}
}

@ -409,7 +409,9 @@ namespace Ombi.Schedule.Jobs.Ombi
private HashSet<IMediaServerEpisode> FilterEpisodes(IEnumerable<IMediaServerEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<IMediaServerEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
foreach (var ep in source.Where(x => x.Series.HasTvDb // needed for recentlyAddedLog
&& x.Series.HasTheMovieDb // needed to fetch info to publish, this is just in case...
))
{
var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId);
if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber))
@ -501,16 +503,11 @@ namespace Ombi.Schedule.Jobs.Ombi
foreach (var content in ordered)
{
int.TryParse(content.TheMovieDbId, out var movieDbId);
if (movieDbId <= 0)
{
_log.LogWarning($"{content.Title} does not have a TMDB ID, it won't be published.");
continue;
}
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId, defaultLanguageCode);
var mediaurl = content.Url;
if (info == null)
{
_log.LogWarning($"TMDB does not know movie {content.Title}, it won't be published.");
_log.LogError($"TMDB does not know movie {content.Title}. This shouldn't happen because our media server knows it as ID '{movieDbId}'.");
continue;
}
try
@ -665,48 +662,18 @@ namespace Ombi.Schedule.Jobs.Ombi
var orderedTv = series.OrderByDescending(x => x.AddedAt);
foreach (var t in orderedTv)
{
if (!t.HasTvDb)
{
// We may need to use themoviedb for the imdbid or their own id to get info
if (t.HasTheMovieDb)
{
int.TryParse(t.TheMovieDbId, out var movieId);
var externals = await _movieApi.GetTvExternals(movieId);
if (externals == null || externals.tvdb_id <= 0)
{
// needed later for recently added log
_log.LogWarning($"{t.Title} has no TVDB ID, it won't be published.");
continue;
}
t.TvDbId = externals.tvdb_id.ToString();
}
// WE could check the below but we need to get the moviedb and then perform the above, let the metadata job figure this out.
//else if(t.HasImdb)
//{
// // Check the imdbid
// var externals = await _movieApi.Find(t.ImdbId, ExternalSource.imdb_id);
// if (externals?.tv_results == null || externals.tv_results.Length <= 0)
// {
// continue;
// }
// t.TvDbId = externals.tv_results.FirstOrDefault()..ToString();
//}
}
try
{
var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId, languageCode);
if (tvInfo == null)
{
_log.LogWarning($"TMDB does not know series {t.Title}, it won't be published.");
_log.LogError($"TMDB does not know series {t.Title}. This shouldn't happen because our media server knows it as ID '{t.TheMovieDbId}'.");
continue;
}
if (tvInfo.backdrop_path.HasValue())
{
AddBackgroundInsideTable($"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}");
}
else
@ -732,7 +699,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
catch (Exception e)
{
_log.LogError(e, "Error when processing Plex TV {0}", t.Title);
_log.LogError(e, "Error when processing TV {0}", t.Title);
}
finally
{

@ -11,5 +11,7 @@ namespace Ombi.Core.Settings.Models.External
public List<int> ExcludedMovieGenreIds { get; set; }
public List<int> ExcludedTvGenreIds { get; set; }
public List<string> OriginalLanguages { get; set; }
}
}

@ -20,5 +20,6 @@ namespace Ombi.Settings.Settings.Models
public static class FeatureNames
{
public const string Movie4KRequests = nameof(Movie4KRequests);
public const string OldTrendingSource = nameof(OldTrendingSource);
}
}

@ -212,7 +212,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Your TV request is now partially available! Season {PartiallyAvailableSeasonNumber} Episodes {PartiallyAvailableEpisodeNumbers}!",
Message = "Your TV request for {Title} is now partially available! Season {PartiallyAvailableSeasonNumber} Episodes {PartiallyAvailableEpisodeNumbers}!",
Subject = "{ApplicationName}: Partially Available Request!",
Agent = agent,
Enabled = true,

@ -102,6 +102,13 @@ namespace Ombi.Store.Repository
return InternalSaveChanges();
}
public override async Task DeleteTv(EmbyContent tv)
{
var episodesToDelete = GetAllEpisodes().Cast<EmbyEpisode>().Where(x => x.ParentId == tv.EmbyId).ToList();
Db.EmbyEpisode.RemoveRange(episodesToDelete);
await Delete(tv);
}
public override RecentlyAddedType RecentlyAddedType => RecentlyAddedType.Emby;
}
}

@ -14,6 +14,7 @@ namespace Ombi.Store.Repository
IQueryable<IMediaServerEpisode> GetAllEpisodes();
Task<IMediaServerEpisode> Add(IMediaServerEpisode content);
Task AddRange(IEnumerable<IMediaServerEpisode> content);
Task DeleteTv(Content tv);
void UpdateWithoutSave(IMediaServerContent existingContent);
}
}

@ -104,6 +104,13 @@ namespace Ombi.Store.Repository
return InternalSaveChanges();
}
public override async Task DeleteTv(JellyfinContent tv)
{
var episodesToDelete = GetAllEpisodes().Cast<JellyfinEpisode>().Where(x => x.ParentId == tv.JellyfinId).ToList();
Db.JellyfinEpisode.RemoveRange(episodesToDelete);
await Delete(tv);
}
public override RecentlyAddedType RecentlyAddedType => RecentlyAddedType.Jellyfin;
}
}

@ -22,5 +22,6 @@ namespace Ombi.Store.Repository
public abstract Task AddRange(IEnumerable<IMediaServerEpisode> content);
public abstract void UpdateWithoutSave(IMediaServerContent existingContent);
public abstract Task UpdateRange(IEnumerable<IMediaServerContent> existingContent);
public abstract Task DeleteTv(T tv);
}
}

@ -169,5 +169,12 @@ namespace Ombi.Store.Repository
Db.PlexServerContent.UpdateRange((IEnumerable<PlexServerContent>)existingContent);
return InternalSaveChanges();
}
public override Task DeleteTv(PlexServerContent tv)
{
// not used for now
// TODO: delete episodes, then delete series
throw new NotImplementedException();
}
}
}

@ -15,14 +15,16 @@ namespace Ombi.Api.TheMovieDb
Task<MovieResponseDto> GetMovieInformation(int movieId);
Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId, string langCode = "en");
Task<List<MovieDbSearchResult>> NowPlaying(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> TrendingMovies(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken));
Task<List<MovieDbSearchResult>> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken));
Task<List<MovieDbSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode);
Task<List<MovieDbSearchResult>> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null);
Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default);
Task<List<MovieDbSearchResult>> TopRated(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> Upcoming(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> UpcomingMovies(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> TopRatedTv(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> TrendingTv(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> UpcomingTv(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> SimilarMovies(int movieId, string langCode);
Task<FindResult> Find(string externalId, ExternalSource source);
@ -41,7 +43,8 @@ namespace Ombi.Api.TheMovieDb
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
Task<List<Genre>> GetGenres(string media, CancellationToken cancellationToken, string languageCode);
Task<List<Language>> GetLanguages(CancellationToken cancellationToken);
Task<List<WatchProvidersResults>> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken);
Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken);
Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, int page, CancellationToken cancellationToken);
}
}

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Ombi.TheMovieDbApi.Models
{
public class Language
{
[JsonProperty("iso_639_1")]
public string Id { get; set; }
[JsonProperty("english_name")]
public string EnglishName { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
}

@ -70,7 +70,7 @@ namespace Ombi.Api.TheMovieDb
public async Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken)
public async Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, int page, CancellationToken cancellationToken)
{
var request = new Request($"discover/{model.Type}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
@ -92,6 +92,9 @@ namespace Ombi.Api.TheMovieDb
}
//request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc");
request.AddQueryString("page", page.ToString());
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
@ -282,21 +285,38 @@ namespace Ombi.Api.TheMovieDb
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
public Task<List<MovieDbSearchResult>> Upcoming(string langCode, int? page = null)
public Task<List<MovieDbSearchResult>> TrendingMovies(string langCode, int? page = null)
{
return Upcoming("movie", langCode, page);
return Trending("movie", langCode, page);
}
public Task<List<MovieDbSearchResult>> UpcomingTv(string langCode, int? page = null)
public Task<List<MovieDbSearchResult>> TrendingTv(string langCode, int? page = null)
{
return Upcoming("tv", langCode, page);
return Trending("tv", langCode, page);
}
private async Task<List<MovieDbSearchResult>> Trending(string type, string langCode, int? page = null)
{
// https://developers.themoviedb.org/3/trending/get-trending
var timeWindow = "week"; // another option can be 'day'
var request = new Request($"trending/{type}/{timeWindow}", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("language", langCode);
if (page != null)
{
request.AddQueryString("page", page.ToString());
}
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
/// <remarks>
/// Maintains filter parity with <a href="https://developers.themoviedb.org/3/movies/get-upcoming">/movie/upcoming</a>.
/// </remarks>
private async Task<List<MovieDbSearchResult>> Upcoming(string type, string langCode, int? page = null)
public async Task<List<MovieDbSearchResult>> UpcomingMovies(string langCode, int? page = null)
{
var request = new Request($"discover/{type}", BaseUri, HttpMethod.Get);
var request = new Request($"discover/movie", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("language", langCode);
@ -313,7 +333,27 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("page", page.ToString());
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
await AddGenreFilter(request, "movie");
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
public async Task<List<MovieDbSearchResult>> UpcomingTv(string langCode, int? page = null)
{
var request = new Request($"discover/tv", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("language", langCode);
// Search for shows that will air in the next month
var startDate = DateTime.Today.AddDays(1);
request.AddQueryString($"first_air_date.gte", startDate.ToString("yyyy-MM-dd"));
request.AddQueryString($"first_air_date.lte", startDate.AddDays(30).ToString("yyyy-MM-dd"));
if (page != null)
{
request.AddQueryString("page", page.ToString());
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, "tv");
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -438,6 +478,16 @@ namespace Ombi.Api.TheMovieDb
return result.genres ?? new List<Genre>();
}
public async Task<List<Language>> GetLanguages(CancellationToken cancellationToken)
{
var request = new Request($"/configuration/languages", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<List<Language>>(request, cancellationToken);
return result ?? new List<Language>();
}
public Task<TheMovieDbContainer<MultiSearch>> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken)
{
var request = new Request("search/multi", BaseUri, HttpMethod.Get);
@ -472,6 +522,10 @@ namespace Ombi.Api.TheMovieDb
{
request.AddQueryString("without_keywords", string.Join(",", settings.ExcludedKeywordIds));
}
if (settings.OriginalLanguages?.Any() == true)
{
request.AddQueryString("with_original_language", string.Join("|", settings.OriginalLanguages));
}
}
private async Task AddGenreFilter(Request request, string media_type)
@ -479,7 +533,8 @@ namespace Ombi.Api.TheMovieDb
var settings = await Settings;
List<int> excludedGenres;
switch (media_type) {
switch (media_type)
{
case "tv":
excludedGenres = settings.ExcludedTvGenreIds;
break;

@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net5.0/ombi.dll",
"program": "${workspaceFolder}/bin/Debug/net6.0/ombi.dll",
"args": ["--host", "http://localhost:3577"],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,

@ -0,0 +1,19 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
"framework": "@storybook/angular",
"core": {
"builder": "@storybook/builder-webpack5"
},
"staticDirs": [{from :'../../wwwroot/images', to: 'images'}],
"features": {
interactionsDebugger: true,
}
}

@ -0,0 +1,9 @@
<style>
.test-class {
background-color: purple;
}
body {
background: #0f171f;
}
</style>

@ -0,0 +1,14 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
setCompodocJson(docJson);
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: { inlineStories: true },
}

@ -0,0 +1,24 @@
{
"extends": "../src/tsconfig.json",
"compilerOptions": {
"types": [
"node"
],
"typeRoots": [
"../node_modules/@typings"
],
"allowSyntheticDefaultImports": true
},
"exclude": [
"../src/test.ts",
"../src/**/*.spec.ts",
"../projects/**/*.spec.ts"
],
"include": [
"../src/**/*",
"../projects/**/*"
],
"files": [
"./typings.d.ts"
]
}

@ -0,0 +1,4 @@
declare module '*.md' {
const content: string;
export default content;
}

@ -19,7 +19,7 @@
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"tsConfig": "src/tsconfig.json",
"assets": [
"src/assets"
],
@ -114,7 +114,7 @@
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json"
"src/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
@ -124,7 +124,6 @@
}
}
},
"defaultProject": "ombi",
"cli": {
"analytics": false
}

@ -0,0 +1,18 @@
{
"pipes": [],
"interfaces": [],
"injectables": [],
"guards": [],
"interceptors": [],
"classes": [],
"directives": [],
"components": [],
"modules": [],
"miscellaneous": [],
"routes": [],
"coverage": {
"count": 0,
"status": "low",
"files": []
}
}

@ -4,37 +4,40 @@
"scripts": {
"ng": "ng",
"start": "ng serve --port 3578 --configuration hmr",
"build": "node --max_old_space_size=6144 node_modules/@angular/cli/bin/ng build --prod",
"lint": "ng lint"
"build": "node --max_old_space_size=6144 node_modules/@angular/cli/bin/ng build -c production",
"lint": "ng lint",
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
"storybook": "start-storybook -p 6006",
"chromatic": "chromatic --exit-zero-on-changes",
"storybookbuild": "yarn build-storybook"
},
"private": true,
"dependencies": {
"@angular/animations": "^13.2.0",
"@angular/animations": "^14.0.0",
"@angular/cdk": "^13.2.0",
"@angular/common": "^13.2.0",
"@angular/compiler": "^13.2.0",
"@angular/core": "^13.2.0",
"@angular/forms": "^13.2.0",
"@angular/localize": "^13.2.0",
"@angular/common": "^14.0.0",
"@angular/compiler": "^14.0.0",
"@angular/core": "^14.0.0",
"@angular/forms": "^14.0.0",
"@angular/localize": "^14.0.0",
"@angular/material": "^13.2.0",
"@angular/platform-browser": "^13.2.0",
"@angular/platform-browser-dynamic": "^13.2.0",
"@angular/platform-server": "^13.2.0",
"@angular/router": "^13.2.0",
"@angular/platform-browser": "^14.0.0",
"@angular/platform-browser-dynamic": "^14.0.0",
"@angular/platform-server": "^14.0.0",
"@angular/router": "^14.0.0",
"@angularclass/hmr": "^3.0.0",
"@aspnet/signalr": "^1.1.0",
"@auth0/angular-jwt": "^5.0.2",
"@fortawesome/fontawesome-free": "^6.0.0",
"@fullcalendar/core": "^4.2.0",
"@fullcalendar/daygrid": "^4.4.0",
"@fullcalendar/interaction": "^4.2.0",
"@microsoft/signalr": "^6.0.7",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@ngxs/devtools-plugin": "^3.7.3",
"@ngxs/store": "^3.7.3",
"@types/jquery": "^3.5.13",
"@yellowspot/ng-truncate": "^2.0.0",
"angular-bootstrap-md": "^7.5.4",
"angular-router-loader": "^0.8.5",
"angularx-qrcode": "^13.0.3",
"bootstrap": "^4.2.1",
@ -48,34 +51,52 @@
"moment": "^2.29.1",
"ng2-cookies": "^1.0.12",
"ngx-clipboard": "^12.1.0",
"ngx-infinite-scroll": "^9.0.0",
"ngx-infinite-scroll": "^14.0.0",
"ngx-moment": "^3.0.1",
"ngx-order-pipe": "^2.2.0",
"popper.js": "^1.14.3",
"primeicons": "^5.0.0",
"primeng": "^13.2.0",
"protractor": "~5.4.0",
"rxjs": "^7.5.4",
"sass-recursive-map-merge": "^1.0.1",
"store": "^2.0.12",
"ts-md5": "^1.2.7",
"ts-node": "~5.0.1",
"tslib": "^1.10.0",
"tslint": "^5.12.0",
"tslint-angular": "^1.1.2",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.2.0",
"@angular/cli": "~13.2.0",
"@angular/compiler-cli": "^13.2.0",
"@angular/language-service": "^13.2.0",
"@angular-devkit/build-angular": "^14.0.0",
"@angular/cli": "^14.0.0",
"@angular/compiler-cli": "^14.0.0",
"@angular/language-service": "^14.0.0",
"@babel/core": "^7.18.9",
"@compodoc/compodoc": "^1.1.19",
"@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.9",
"@storybook/addon-interactions": "^6.5.9",
"@storybook/addon-links": "^6.5.9",
"@storybook/angular": "^6.5.9",
"@storybook/builder-webpack5": "^6.5.9",
"@storybook/jest": "^0.0.10",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"@types/jasmine": "~3.6.7",
"@types/jasminewd2": "~2.0.8",
"@types/node": "^16.10.9",
"@types/node": "^16.11.45",
"babel-loader": "^8.2.5",
"chromatic": "^6.7.1",
"codelyzer": "^6.0.1",
"typescript": "~4.5.5"
"typescript": "~4.7.3"
},
"optionalDependencies": {
"protractor": "~5.4.0",
"ts-node": "~5.0.1",
"tslint": "^5.12.0"
}
},
"readme": "ERROR: No README data found!",
"_id": "ombi@3.0.0"
}

@ -1,5 +1,4 @@
import { APP_BASE_HREF, CommonModule, PlatformLocation } from "@angular/common";
import { CardsFreeModule, MDBBootstrapModule, NavbarModule } from "angular-bootstrap-md";
import { CustomPageService, ImageService, RequestService, SettingsService } from "./services";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http";
@ -69,6 +68,7 @@ import { TooltipModule } from "primeng/tooltip";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { TranslateService } from "@ngx-translate/core";
import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor";
import { ImageBackgroundComponent, ImageComponent } from "./components/";
import { environment } from "../environments/environment";
const routes: Routes = [
@ -131,7 +131,6 @@ export function JwtTokenGetter() {
MatSnackBarModule,
DialogModule,
MatButtonModule,
NavbarModule,
MatCardModule,
MatTooltipModule,
MatMenuModule,
@ -145,11 +144,9 @@ export function JwtTokenGetter() {
ConfirmDialogModule,
OverlayPanelModule,
CommonModule,
CardsFreeModule,
OverlayModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(),
JwtModule.forRoot({
config: {
tokenGetter: JwtTokenGetter,
@ -170,7 +167,9 @@ export function JwtTokenGetter() {
...environment.production ? [] :
[
NgxsReduxDevtoolsPluginModule.forRoot(),
]
],
ImageBackgroundComponent,
ImageComponent,
],
declarations: [
AppComponent,

@ -0,0 +1,6 @@
<div @fadeInOut class="bg" [style.background-image]="background">
<div class="login-gradient-bar">
</div>
<div class="poster-desc">{{name}}</div>
</div>

@ -0,0 +1,26 @@
.login-gradient-bar{
background: linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.6) 20.0%, rgba(0,0,0,0.6) 80.0%, transparent 60%),transparent;
height:100%;
width:100%;
position: absolute;
}
.bg {
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
height: 100vh;
width: 100vw;
position: fixed;
}
.poster-desc {
padding-left: 1%;
color: white;
height: 100vh;
width: 100vw;
display: flex;
justify-content: end;
flex-direction: column;
}

@ -0,0 +1,43 @@
import { OmbiCommonModules } from "../modules";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ImageService } from "../../services";
import { fadeInOutAnimation } from "app/animations/fadeinout";
@Component({
standalone: true,
selector: 'ombi-image-background',
templateUrl: './image-background.component.html',
styleUrls: ['./image-background.component.scss'],
imports: [...OmbiCommonModules, BrowserAnimationsModule],
providers: [ ImageService ],
animations: [ fadeInOutAnimation ],
})
export class ImageBackgroundComponent implements OnInit, OnDestroy {
public background: any;
public name: string;
private timer: NodeJS.Timer;
constructor(private images: ImageService, private sanitizer: DomSanitizer) { }
public ngOnDestroy(): void {
clearTimeout(this.timer);
}
public ngOnInit(): void {
this.cycleBackground();
this.timer = setInterval(() => {
this.cycleBackground();
}, 30000);
}
private cycleBackground() {
this.images.getRandomBackgroundWithInfo().subscribe((x) => {
this.background = this.sanitizer.bypassSecurityTrustStyle("url(" + x.url + ")");
this.name = x.name;
});
}
}

@ -0,0 +1 @@
<img src="{{src}}" (error)="onError($event)" [class]="class" [id]="id" [style]="style"/>

@ -0,0 +1,73 @@
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { APP_BASE_HREF } from '@angular/common';
import { Story, Meta, moduleMetadata } from '@storybook/angular';
import { RequestType } from '../../interfaces';
import { ImageComponent } from './image.component';
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
title: 'Image Component',
component: ImageComponent,
decorators: [
moduleMetadata({
providers: [
{
provide: APP_BASE_HREF,
useValue: ""
},
]
})
]
} as Meta;
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<ImageComponent> = (args: ImageComponent) => ({
props: args,
});
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
Primary.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie
};
export const ClassApplied = Template.bind({});
ClassApplied.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie,
class: 'test-class'
};
export const StyleApplied = Template.bind({});
StyleApplied.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie,
style: 'background-color: red;'
};
export const IdApplied = Template.bind({});
IdApplied.args = {
src: 'https://ombi.io/img/logo-orange-small.png',
type: RequestType.movie,
id: 'testId123'
};
// export const InvalidMovieImage = Template.bind({});
// InvalidMovieImage.args = {
// src: 'https://httpstat.us/429',
// type: RequestType.movie,
// id: 'testId123'
// };
// export const InvalidTvImage = Template.bind({});
// InvalidTvImage.args = {
// src: 'https://httpstat.us/429',
// type: RequestType.tvShow,
// };
// export const InvalidMusicImage = Template.bind({});
// InvalidMusicImage.args = {
// src: 'https://httpstat.us/429',
// type: RequestType.album,
// };

@ -0,0 +1,57 @@
import { OmbiCommonModules } from "../modules";
import { ChangeDetectionStrategy, Component, ElementRef, Inject, Input, ViewEncapsulation } from "@angular/core";
import { RequestType } from "../../interfaces";
import { APP_BASE_HREF } from "@angular/common";
@Component({
standalone: true,
selector: 'ombi-image',
imports: [...OmbiCommonModules],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './image.component.html',
})
export class ImageComponent {
@Input() public src: string;
@Input() public type: RequestType;
// Attributes from the parent
@Input() public class: string;
@Input() public id: string;
@Input() public alt: string;
@Input() public style: string;
public baseUrl: string = "";
public defaultTv = "/images/default_tv_poster.png";
private defaultMovie = "/images/default_movie_poster.png";
private defaultMusic = "i/mages/default-music-placeholder.png";
constructor (@Inject(APP_BASE_HREF) public href: string) {
if (this.href.length > 1) {
this.baseUrl = this.href;
}
}
public onError(event: any) {
// set to a placeholder
switch(this.type) {
case RequestType.movie:
event.target.src = this.baseUrl + this.defaultMovie;
break;
case RequestType.tvShow:
event.target.src = this.baseUrl + this.defaultTv;
break;
case RequestType.album:
event.target.src = this.baseUrl + this.defaultMusic;
break;
}
// Retry the original image
const timeout = setTimeout(() => {
event.target.src = this.src;
clearTimeout(timeout);
}, Math.floor(Math.random() * (7000 - 1000 + 1)) + 1000);
}
}

@ -0,0 +1,2 @@
export * from "./image-background/image-background.component";
export * from "./image/image.component";

@ -0,0 +1,3 @@
import { CommonModule } from "@angular/common";
export const OmbiCommonModules = [ CommonModule ];

@ -1,5 +1,5 @@
import { Component, OnInit, SecurityContext } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { DomSanitizer } from "@angular/platform-browser";
import { AuthService } from "../auth/auth.service";
import { CustomPageService, NotificationService } from "../services";
@ -10,11 +10,11 @@ import { CustomPageService, NotificationService } from "../services";
})
export class CustomPageComponent implements OnInit {
public form: FormGroup;
public form: UntypedFormGroup;
public isEditing: boolean;
public isAdmin: boolean;
constructor(private auth: AuthService, private settings: CustomPageService, private fb: FormBuilder,
constructor(private auth: AuthService, private settings: CustomPageService, private fb: UntypedFormBuilder,
private notificationService: NotificationService,
private sanitizer: DomSanitizer) {
}

@ -2,15 +2,14 @@
<div id="result{{result.id}}" *ngIf="fullyLoaded" class="ombi-card dark-card c" [style.display]="hide ? 'none' : 'block'">
<div class="card-top-info">
<div class="top-left" id="type{{result.id}}">
<div class="top-left" id="type{{result.id}}-{{RequestType[result.type]}}">
{{ 'Common.' + RequestType[result.type] | translate }}
</div>
<div class="{{getStatusClass()}} top-right" id="status{{result.id}}">
<span class="indicator"></span><span class="indicator-text" id="availabilityStatus{{result.id}}">{{getAvailabilityStatus()}}</span>
</div>
</div>
<img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image"
alt="{{result.title}}">
<ombi-image [src]="result.posterPath" [type]="result.type" [routerLink]="generateDetailsLink()" id="cardImage" class="image" alt="{{result.title}}"></ombi-image>
<div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'">
<a class="poster-overlay" [routerLink]="generateDetailsLink()">
<div class="summary">

@ -83,7 +83,7 @@ small {
}
.image {
::ng-deep .image {
border-radius: 10px;
opacity: 1;
display: block;

@ -169,6 +169,19 @@ export class DiscoverCardComponent implements OnInit {
}
}
public onImageError(event: any) {
const originalSrc = event.target.src;
// set to a placeholder
event.target.src = "../../../images/default_movie_poster.png";
// Retry the original image
const timeout = setTimeout(() => {
event.target.src = originalSrc;
clearTimeout(timeout);
}, Math.floor(Math.random() * (7000 - 1000 + 1)) + 1000);
}
private getExtraMovieInfo() {
if (!this.result.imdbid) {
this.searchService.getFullMovieDetails(+this.result.id)

@ -48,6 +48,7 @@ export class CarouselListComponent implements OnInit {
constructor(private searchService: SearchV2Service,
private storageService: StorageService,
private featureFacade: FeaturesFacade) {
Carousel.prototype.onTouchMove = () => { },
this.responsiveOptions = [
{
breakpoint: '4000px',

@ -2,7 +2,13 @@
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner>
</div>
<div *ngIf="discoverResults.length > 0" class="row full-height discoverResults col" >
<div *ngIf="discoverResults.length > 0"
class="row full-height discoverResults col"
infiniteScroll
[infiniteScrollDistance]="3"
[infiniteScrollThrottle]="200"
(scrolled)="onScroll()">
<div id="searchResults" class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults" data-test="searchResultsCount" attr.search-count="{{discoverResults.length}}">
<discover-card [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card>
</div>

@ -29,6 +29,7 @@ export class DiscoverSearchResultsComponent implements OnInit {
public filter: SearchFilter;
private isAdvancedSearch: boolean;
private loadPosition: number = 30;
constructor(private searchService: SearchV2Service,
private route: ActivatedRoute,
@ -65,7 +66,7 @@ export class DiscoverSearchResultsComponent implements OnInit {
}
});
if (this.advancedDataService) {
if (this.isAdvancedSearch) {
return;
}
this.loadingFlag = true;
@ -179,6 +180,31 @@ export class DiscoverSearchResultsComponent implements OnInit {
});
}
public onScroll() {
console.log("scrolled");
if (this.advancedDataService) {
this.loadMoreAdvancedSearch();
return;
}
}
private loadMoreAdvancedSearch() {
const advancedOptions = this.advancedDataService.getOptions();
this.searchService.advancedSearch({
type: advancedOptions.type == RequestType.movie ? "movie" : "tv",
companies: advancedOptions.companies,
genreIds: advancedOptions.genres,
keywordIds : advancedOptions.keywords,
releaseYear: advancedOptions.releaseYear,
watchProviders: advancedOptions.watchProviders,
}, this.loadPosition, 30).then(x => {
this.loadPosition += 30;
this.mapAdvancedData(x);
});
}
private async search() {
this.clear();
this.results = await this.searchService

@ -8,6 +8,7 @@ import { PipeModule } from "../pipes/pipe.module";
import { RouterModule } from "@angular/router";
import { SharedModule } from "../shared/shared.module";
import { SkeletonModule } from 'primeng/skeleton';
import { ImageComponent } from 'app/components';
@NgModule({
imports: [
@ -18,6 +19,7 @@ import { SkeletonModule } from 'primeng/skeleton';
MatButtonToggleModule,
InfiniteScrollModule,
SkeletonModule,
ImageComponent
],
declarations: [
...fromComponents.components

@ -1,3 +1,7 @@
export interface IImages {
url: string;
}
export interface IImagesInfo {
url: string;
name: string;
}

@ -17,3 +17,8 @@ export interface IDiscoverModel {
watchProviders?: number[];
companies?: number[];
}
export interface ILanguage {
iso_639_1 : string;
english_name : string;
name : string;
}

@ -339,7 +339,8 @@ export interface ITheMovieDbSettings extends ISettings {
showAdultMovies: boolean;
excludedKeywordIds: number[];
excludedMovieGenreIds: number[];
excludedTvGenreIds: number[]
excludedTvGenreIds: number[];
originalLanguages: string[];
}
export interface IUpdateModel

@ -14,7 +14,7 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button color="accent" [routerLink]="[element.providerId]">{{ 'Issues.Details' | translate}}</button>
<a mat-raised-button color="accent" [routerLink]="[element.providerId]">{{ 'Issues.Details' | translate}}</a>
</td>
</ng-container>

@ -1,5 +1,5 @@
<div *ngIf="landingPageSettings && customizationSettings">
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background"></div>
<div *ngIf="landingPageSettings && customizationSettings" style="overflow:hidden">
<ombi-image-background></ombi-image-background>
<div class="small-middle-container">
<div class="row">

@ -26,15 +26,6 @@ div.centered {
transform: translate(-50%, -50%);
}
div.bg {
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
height: 100vh;
width: 100vw;
position: fixed;
}
.online{
color:lightgreen;

@ -1,48 +1,34 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Component, OnDestroy, OnInit, Inject } from "@angular/core";
import { APP_BASE_HREF } from "@angular/common";
import { Component, OnInit, Inject } from "@angular/core";
import { IMediaServerStatus } from "../interfaces";
import { ICustomizationSettings, ILandingPageSettings } from "../interfaces";
import { LandingPageService } from "../services";
import { SettingsService } from "../services";
import { DomSanitizer } from "@angular/platform-browser";
import { ImageService } from "../services";
import { fadeInOutAnimation } from "../animations/fadeinout";
import { CustomizationFacade } from "../state/customization";
import { ThousandShortPipe } from "../pipes/ThousandShortPipe";
@Component({
templateUrl: "./landingpage.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./landingpage.component.scss"],
})
export class LandingPageComponent implements OnDestroy, OnInit {
export class LandingPageComponent implements OnInit {
public customizationSettings: ICustomizationSettings;
public landingPageSettings: ILandingPageSettings;
public background: any;
public mediaServerStatus: IMediaServerStatus;
public baseUrl: string;
private timer: any;
private href: string;
constructor(private settingsService: SettingsService,
private images: ImageService, private sanitizer: DomSanitizer, private landingPageService: LandingPageService,
private landingPageService: LandingPageService,
private customizationFacade: CustomizationFacade,
@Inject(APP_BASE_HREF) href :string) { this.href = href }
public ngOnInit() {
this.customizationFacade.settings$().subscribe(x => this.customizationSettings = x);
this.settingsService.getLandingPage().subscribe(x => this.landingPageSettings = x);
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 19%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 79%, transparent 80%), url(" + x.url + ")");
});
this.timer = setInterval(() => {
this.cycleBackground();
}, 30000);
const base = this.href;
if (base.length > 1) {
@ -53,18 +39,4 @@ export class LandingPageComponent implements OnDestroy, OnInit {
this.mediaServerStatus = x;
});
}
public ngOnDestroy() {
clearInterval(this.timer);
}
public cycleBackground() {
this.images.getRandomBackground().subscribe(x => {
this.background = "";
});
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer
.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")");
});
}
}

@ -1,7 +1,4 @@
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background">
<div class="login-gradient-bar">
</div>
</div>
<ombi-image-background></ombi-image-background>
<div class="small-middle-container">
<div *ngIf="form && customizationSettings && authenticationSettings">

@ -11,23 +11,6 @@ img.center {
max-width: 100%;
}
.login-gradient-bar{
background: linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.6) 20.0%, rgba(0,0,0,0.6) 80.0%, transparent 60%),transparent;
height:100%;
width:100%;
position: absolute;
}
div.bg {
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
height: 100vh;
width: 100vw;
position: fixed;
}
.card-container.card {
max-width: 500px;
padding: 45px 45px;

@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit, Inject } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { TranslateService } from "@ngx-translate/core";
@ -10,25 +10,19 @@ import { PlexTvService } from "../services";
import { SettingsService } from "../services";
import { StatusService } from "../services";
import { DomSanitizer } from "@angular/platform-browser";
import { ImageService } from "../services";
import { fadeInOutAnimation } from "../animations/fadeinout";
import { StorageService } from "../shared/storage/storage-service";
import { MatSnackBar } from "@angular/material/snack-bar";
import { CustomizationFacade } from "../state/customization";
@Component({
templateUrl: "./login.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./login.component.scss"],
})
export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup;
public form: UntypedFormGroup;
public customizationSettings: ICustomizationSettings;
public authenticationSettings: IAuthenticationSettings;
public plexEnabled: boolean;
public background: any;
public landingFlag: boolean;
public baseUrl: string;
public loginWithOmbi: boolean;
@ -46,7 +40,6 @@ export class LoginComponent implements OnDestroy, OnInit {
public get appNameTranslate(): object {
return { appName: this.appName };
}
private timer: any;
private clientId: string;
private errorBody: string;
@ -59,11 +52,9 @@ export class LoginComponent implements OnDestroy, OnInit {
private authService: AuthService,
private router: Router,
private status: StatusService,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private settingsService: SettingsService,
private customziationFacade: CustomizationFacade,
private images: ImageService,
private sanitizer: DomSanitizer,
private route: ActivatedRoute,
@Inject(APP_BASE_HREF) href: string,
private translate: TranslateService,
@ -111,14 +102,6 @@ export class LoginComponent implements OnDestroy, OnInit {
this.headerAuth();
});
this.settingsService.getClientId().subscribe((x) => (this.clientId = x));
this.images.getRandomBackground().subscribe((x) => {
this.background = this.sanitizer.bypassSecurityTrustStyle(
"url(" + x.url + ")"
);
});
this.timer = setInterval(() => {
this.cycleBackground();
}, 30000);
const base = this.href;
if (base.length > 1) {
@ -132,7 +115,7 @@ export class LoginComponent implements OnDestroy, OnInit {
.subscribe((x) => (this.errorValidation = x));
}
public onSubmit(form: FormGroup) {
public onSubmit(form: UntypedFormGroup) {
if (form.invalid) {
this.notify.open(this.errorValidation, "OK", {
duration: 300000,
@ -284,18 +267,6 @@ export class LoginComponent implements OnDestroy, OnInit {
}
public ngOnDestroy() {
clearInterval(this.timer);
clearInterval(this.pinTimer);
}
private cycleBackground() {
this.images.getRandomBackground().subscribe((x) => {
this.background = "";
});
this.images.getRandomBackground().subscribe((x) => {
this.background = this.sanitizer.bypassSecurityTrustStyle(
"url(" + x.url + ")"
);
});
}
}

@ -1,8 +1,5 @@

<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background">
<div class="login-gradient-bar">
</div>
</div>
<ombi-image-background></ombi-image-background>
<div class="small-middle-container">
<div *ngIf="form && customizationSettings">

@ -1,30 +1,27 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { APP_BASE_HREF } from "@angular/common";
import { Component, OnInit, Inject } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { DomSanitizer } from "@angular/platform-browser";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { fadeInOutAnimation } from "../animations/fadeinout";
import { ICustomizationSettings } from "../interfaces";
import { IdentityService, ImageService, NotificationService, SettingsService } from "../services";
import { IdentityService, NotificationService, SettingsService } from "../services";
import { CustomizationFacade } from "../state/customization";
@Component({
templateUrl: "./resetpassword.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./login.component.scss"],
})
export class ResetPasswordComponent implements OnInit {
public form: FormGroup;
public form: UntypedFormGroup;
public customizationSettings: ICustomizationSettings;
public emailSettingsEnabled: boolean;
public baseUrl: string;
public background: any;
private href: string;
constructor(private identityService: IdentityService, private notify: NotificationService,
private fb: FormBuilder, private settingsService: SettingsService, @Inject(APP_BASE_HREF) href:string,
private images: ImageService, private sanitizer: DomSanitizer, private customizationFacade: CustomizationFacade) {
private fb: UntypedFormBuilder, private settingsService: SettingsService, @Inject(APP_BASE_HREF) href:string,
private customizationFacade: CustomizationFacade) {
this.href = href;
this.form = this.fb.group({
email: ["", [Validators.required]],
@ -32,9 +29,7 @@ export class ResetPasswordComponent implements OnInit {
}
public ngOnInit() {
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%),url(" + x.url + ")");
});
const base = this.href;
if (base.length > 1) {
this.baseUrl = base;
@ -43,7 +38,7 @@ export class ResetPasswordComponent implements OnInit {
this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailSettingsEnabled = x);
}
public onSubmit(form: FormGroup) {
public onSubmit(form: UntypedFormGroup) {
if (this.emailSettingsEnabled) {
if (form.invalid) {

@ -1,8 +1,4 @@

<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background">
<div class="login-gradient-bar">
</div>
</div>
<ombi-image-background></ombi-image-background>
<div class="small-middle-container">
<div *ngIf="form && customizationSettings">
@ -41,8 +37,6 @@
Password is <strong>required</strong></mat-error>
</mat-form-field>
<button id="reset" mat-raised-button color="accent" [disabled]="form.invalid" type="submit">{{'Reset Password' | translate}}</button>
</form>

@ -1,10 +1,9 @@
import { ActivatedRoute, Params } from "@angular/router";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { IdentityService, ImageService } from "../services";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { IdentityService } from "../services";
import { CustomizationFacade } from "../state/customization";
import { DomSanitizer } from "@angular/platform-browser";
import { ICustomizationSettings } from "../interfaces";
import { IResetPasswordToken } from "../interfaces";
import { NotificationService } from "../services";
@ -17,15 +16,12 @@ import { Router } from "@angular/router";
})
export class TokenResetPasswordComponent implements OnInit {
public form: FormGroup;
public form: UntypedFormGroup;
public customizationSettings: ICustomizationSettings;
public background: any;
public baseUrl: string;
constructor(private identityService: IdentityService, private router: Router, private route: ActivatedRoute, private notify: NotificationService,
private fb: FormBuilder, private location: PlatformLocation, private images: ImageService,
private sanitizer: DomSanitizer, private customizationFacade: CustomizationFacade,
) {
private fb: UntypedFormBuilder, private location: PlatformLocation, private customizationFacade: CustomizationFacade) {
this.route.queryParams
.subscribe((params: Params) => {
@ -39,9 +35,6 @@ export class TokenResetPasswordComponent implements OnInit {
}
public ngOnInit() {
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%),url(" + x.url + ")");
});
const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) {
this.baseUrl = base;
@ -49,7 +42,7 @@ export class TokenResetPasswordComponent implements OnInit {
this.customizationFacade.settings$().subscribe(x => this.customizationSettings = x);
}
public onSubmit(form: FormGroup) {
public onSubmit(form: UntypedFormGroup) {
if (form.invalid) {
this.notify.error("Email address is required");
return;
@ -65,6 +58,5 @@ export class TokenResetPasswordComponent implements OnInit {
});
}
});
}
}

@ -10,9 +10,8 @@
<social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id"
[hasTrailer]="movie.videos?.results?.length > 0" [imdbId]="movie.imdbId"
[twitter]="movie.externalIds.twitterId" [facebook]="movie.externalIds.facebookId"
[instagram]="movie.externalIds.instagramId" [available]="movie.available" [plexUrl]="movie.plexUrl"
[embyUrl]="movie.embyUrl" [jellyfinUrl]="movie.jellyfinUrl" [isAdmin]="isAdmin"
[twitter]="movie.externalIds?.twitterId" [facebook]="movie.externalIds?.facebookId"
[instagram]="movie.externalIds?.instagramId" [available]="movie.available" [isAdmin]="isAdmin"
[canShowAdvanced]="showAdvanced && movieRequest" [type]="requestType" [has4KRequest]="movie.has4KRequest"
(openTrailer)="openDialog()" (onAdvancedOptions)="openAdvancedOptions()"
(onReProcessRequest)="reProcessRequest(false)" (onReProcess4KRequest)="reProcessRequest(true)">
@ -24,7 +23,7 @@
<div class="row justify-content-center justify-content-sm-start header-container">
<div class="details-poster-container">
<media-poster [posterPath]="'https://image.tmdb.org/t/p/w300/' + movie.posterPath"></media-poster>
<media-poster [posterPath]=movie.posterPath></media-poster>
</div>
<!--Next to poster-->
@ -239,44 +238,44 @@
</div>
<mat-accordion class=" mat-elevation-z8 spacing-below ">
<mat-expansion-panel>
<mat-expansion-panel *ngIf="movie.recommendations?.results?.length> 0">
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.RecommendationsTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer " *ngIf="movie.recommendations?.results?.length> 0">
<div class="row card-spacer ">
<div class="col-md-2" *ngFor="let r of movie.recommendations?.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster">
<a [routerLink]="'/details/movie/'+r.id">
<img class="real grow" matTooltip="{{r.title}}"
<ombi-image class="real grow" matTooltip="{{r.title}}"
src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}"
alt="Poster" style="display: block;">
alt="Poster" style="display: block;"> </ombi-image>
</a>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel *ngIf="movie.similar?.results?.length > 0">
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.SimilarTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.similar?.results?.length > 0">
<div class="row card-spacer">
<div class="col-md-2" *ngFor="let r of movie.similar.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster ">
<a [routerLink]="'/details/movie/'+r.id">
<img class="real grow" matTooltip="{{r.title}}"
<ombi-image class="real grow" matTooltip="{{r.title}}"
src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}"
alt="Poster" style="display: block;">
alt="Poster" style="display: block;"></ombi-image>
</a>
</div>
</div>

@ -1,6 +1,6 @@
import { AfterViewInit, Component, OnInit, ViewChild, ViewEncapsulation } from "@angular/core";
import { Component, OnInit, ViewEncapsulation } from "@angular/core";
import { ImageService, SearchV2Service, RequestService, MessageService, RadarrService, SettingsStateService } from "../../../services";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { MatDialog } from "@angular/material/dialog";
@ -12,7 +12,6 @@ import { NewIssueComponent } from "../shared/new-issue/new-issue.component";
import { TranslateService } from "@ngx-translate/core";
import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component";
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
import { firstValueFrom, forkJoin } from "rxjs";
import { AdminRequestDialogComponent } from "../../../shared/admin-request-dialog/admin-request-dialog.component";
import { FeaturesFacade } from "../../../state/features/features.facade";
@ -32,31 +31,45 @@ export class MovieDetailsComponent implements OnInit{
public issuesEnabled: boolean;
public roleName4k = "Request4KMovie";
public is4KEnabled = false;
public requestType = RequestType.movie;
private theMovidDbId: number;
private imdbId: string;
private snapMovieId: string;
constructor(private searchService: SearchV2Service, private route: ActivatedRoute,
constructor(private searchService: SearchV2Service, private route: ActivatedRoute, private router: Router,
private sanitizer: DomSanitizer, private imageService: ImageService,
public dialog: MatDialog, private requestService: RequestService,
private requestService2: RequestServiceV2, private radarrService: RadarrService,
public messageService: MessageService, private auth: AuthService, private settingsState: SettingsStateService,
private translate: TranslateService, private featureFacade: FeaturesFacade) {
this.snapMovieId = this.route.snapshot.params.movieDbId;
this.route.params.subscribe(async (params: any) => {
if (typeof params.movieDbId === 'string' || params.movieDbId instanceof String) {
if (params.movieDbId.startsWith("tt")) {
this.imdbId = params.movieDbId;
// Check if we user navigated to another movie and if so reload the component
if (this.imdbId !== this.snapMovieId) {
this.reloadComponent()
}
}
}
this.theMovidDbId = params.movieDbId;
// Check if we user navigated to another movie and if so reload the component
if (params.movieDbId !== this.snapMovieId) {
this.reloadComponent()
}
});
}
async ngOnInit() {
reloadComponent() {
let currentUrl = this.router.url;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.router.navigate([currentUrl]);
}
async ngOnInit() {
this.is4KEnabled = this.featureFacade.is4kEnabled();
this.issuesEnabled = this.settingsState.getIssue();
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
@ -68,6 +81,7 @@ export class MovieDetailsComponent implements OnInit{
if (this.imdbId) {
this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => {
this.movie = x;
this.checkPoster();
if (this.movie.requestId > 0) {
// Load up this request
this.hasRequest = true;
@ -78,6 +92,7 @@ export class MovieDetailsComponent implements OnInit{
} else {
this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(async x => {
this.movie = x;
this.checkPoster();
if (this.movie.requestId > 0) {
// Load up this request
this.hasRequest = true;
@ -272,7 +287,14 @@ export class MovieDetailsComponent implements OnInit{
}
});
}
private checkPoster() {
if (this.movie.posterPath == null) {
this.movie.posterPath = "../../../images/default_movie_poster.png";
}
else {
this.movie.posterPath = "https://image.tmdb.org/t/p/w300/" + this.movie.posterPath
};
}
private loadAdvancedInfo() {
const profile = this.radarrService.getQualityProfilesFromSettings();
const folders = this.radarrService.getRootFoldersFromSettings();

@ -31,16 +31,22 @@
</div>
<div>
<span class="label">{{'MediaDetails.Availability' | translate }}</span>
<span *ngIf="movie.available"> {{'Common.Available' | translate}}</span>
<span *ngIf="!movie.available"> {{'Common.NotAvailable' | translate}}</span>
<span *ngIf="movie.available || movie.available4K"> {{'Common.Available' | translate}}</span>
<span *ngIf="!movie.available && !movie.available4K"> {{'Common.NotAvailable' | translate}}</span>
</div>
<div *ngIf="!movie.available">
<div *ngIf="(!movie.available && movie.requested) || (!movie.available4K && movie.has4KRequest)">
<span class="label">{{'MediaDetails.RequestStatus' | translate }}</span>
<div *ngIf="movie.denied">{{'Common.Denied' | translate}}</div>
<div *ngElseIf="movie.approved && !movie.available">{{'Common.ProcessingRequest' | translate}}</div>
<div *ngElseIf="movie.requested && !movie.approved && !movie.available">{{'Common.PendingApproval' | translate}}
<div *ngIf="!movie.available && movie.requested">
<div *ngIf="movie.denied">{{'Common.RequestDenied' | translate}}</div>
<div *ngIf="movie.approved && !movie.available">{{'Common.ProcessingRequest' | translate}}</div>
<div *ngIf="movie.requested && !movie.approved">{{'Common.PendingApproval' | translate}}</div>
<!--<div *ngIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}}</div>-->
</div>
<div *ngElseIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}}
<div *ngIf="!movie.available4K && movie.has4KRequest">
<div *ngIf="movie.denied4K">{{'Common.RequestDenied4K' | translate}}</div>
<div *ngIf="movie.approved4K && !movie.available4K">{{'Common.ProcessingRequest4K' | translate}}</div>
<div *ngIf="movie.requested4K && !movie.approved4K && !movie.available4K">{{'Common.PendingApproval4K' | translate}}</div>
<!--<div *ngIf="!movie.requested4K && !movie.available4K && !movie.approved4K">{{'Common.NotRequested4K' | translate}}</div>-->
</div>
</div>
@ -51,7 +57,8 @@
<div *ngIf="request">
<span class="label">{{'MediaDetails.RequestDate' | translate }}</span>
{{request.requestedDate | amUserLocale | amDateFormat: 'LL'}}
<span *ngIf="request.requestedDate < request.requestedDate4k"> {{request.requestedDate4k | amUserLocale | amDateFormat: 'LL'}}</span>
<span *ngIf="request.requestedDate > request.requestedDate4k"> {{request.requestedDate | amUserLocale | amDateFormat: 'LL'}}</span>
</div>
<div *ngIf="request && request.source !== RequestSource.Ombi">
@ -59,10 +66,15 @@
{{RequestSource[request.source]}}
</div>
<div *ngIf="request && request.denied">
<div *ngIf="request">
<span class="label">{{'MediaDetails.DeniedReason' | translate }}</span>
<div *ngIf="request.denied">
<span id="deniedReasonInfo">{{request.deniedReason}}</span>
</div>
<div *ngIf="request.denied4K">
<span id="deniedReasonInfo4K">{{request.deniedReason4K}}</span>
</div>
</div>
<div *ngIf="movie.quality">

@ -6,10 +6,10 @@
<div class="row justify-content-md-center mat-card mat-card-flat carousel-item">
<div class="bottom-space">
<a *ngIf="item.image" [routerLink]="'/discover/actor/' + item.id">
<img class="cast-profile-img" src="https://image.tmdb.org/t/p/w300{{item.image}}">
<ombi-image class="cast-profile-img" src="https://image.tmdb.org/t/p/w300{{item.image}}"></ombi-image>
</a>
<a *ngIf="item.profile_path" [routerLink]="'/discover/actor/' + item.id">
<img class="cast-profile-img" src="https://image.tmdb.org/t/p/w300{{item.profile_path}}">
<ombi-image class="cast-profile-img" src="https://image.tmdb.org/t/p/w300{{item.profile_path}}"></ombi-image>
</a>
<!-- TODO get profile image default -->

@ -65,7 +65,7 @@ body .ui-carousel .ui-carousel-content .ui-carousel-next:not(.ui-state-disabled)
@media (min-width: 979px) {
.cast-profile-img {
:host ::ng-deep .cast-profile-img {
border-radius: 100%;
width: 200px;
max-height: 200px;
@ -74,7 +74,7 @@ body .ui-carousel .ui-carousel-content .ui-carousel-next:not(.ui-state-disabled)
}
@media (max-width: 978px) {
.cast-profile-img {
:host ::ng-deep .cast-profile-img {
border-radius: 100%;
width: 100px;
max-height: 100px;

@ -1,8 +1,8 @@
<div class="col-md-2 col-sm-3 hidden-xs">
<div class="sidebar sidebar-poster affixable affix-top">
<div class="poster mobile-poster">
<img class="real" src="{{posterPath}}" alt="Poster"
style="display: block;">
<ombi-image class="real" src="{{posterPath}}" alt="Poster"
style="display: block;"></ombi-image>
</div>
<!--Underneith poster-->
<br />

@ -1,13 +1,10 @@
import { Component, Inject, OnInit } from "@angular/core";
import { IDenyDialogData } from "../interfaces/interfaces";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { RequestService, MessageService, IdentityService } from "../../../../services";
import { RequestType, IRequestEngineResult, IUserDropdown } from "../../../../interfaces";
import { FormControl } from "@angular/forms";
import { Component, OnInit } from "@angular/core";
import { MatDialogRef } from "@angular/material/dialog";
import { IdentityService } from "../../../../services";
import { IUserDropdown } from "../../../../interfaces";
import { UntypedFormControl } from "@angular/forms";
import { Observable } from "rxjs";
import { filter, map, startWith } from "rxjs/operators";
import { map, startWith } from "rxjs/operators";
@Component({
selector: "request-behalf",
@ -18,7 +15,7 @@ export class RequestBehalfComponent implements OnInit {
public dialogRef: MatDialogRef<RequestBehalfComponent>,
public identity: IdentityService) { }
public myControl = new FormControl();
public myControl = new UntypedFormControl();
public options: IUserDropdown[];
public filteredOptions: Observable<IUserDropdown[]>;
public userId: string;

@ -11,7 +11,7 @@
<i matTooltip="The TV DB" class="fas fa-tv fa-2x grow-social"></i>
</a>
<a *ngIf="hasTrailer" class="media-icons youtube" (click)="openDialog()">
<a *ngIf="hasTrailer" data-testid="social-trailer" class="media-icons youtube" (click)="openDialog()">
<i matTooltip="Trailer" class="fab fa-youtube fa-2x grow-social"></i>
</a>
<a *ngIf="imdbId" class="media-icons imdb" [href]="doNotAppend ? imdbid : 'https://imdb.com/title/' + imdbId" target="_blank">

@ -14,6 +14,8 @@ a.media-icons img{
padding: 0;
}
button.admin-cog{
margin-left:40px;
color:$ombi-active;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save