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

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

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

1
.gitignore vendored

@ -251,3 +251,4 @@ _Pvt_Extensions
/src/Ombi/databases.json /src/Ombi/databases.json
/src/Ombi/healthchecksdb /src/Ombi/healthchecksdb
/src/Ombi/ClientApp/package-lock.json /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.22.4](https://github.com/Ombi-app/Ombi/compare/v4.22.3...v4.22.4) (2022-08-04)
## [4.16.11](https://github.com/Ombi-app/Ombi/compare/v4.16.10...v4.16.11) (2022-04-14)
### Bug Fixes ### Bug Fixes
* Set the default job for the watchlist import to hourly instead of daily ([75906af](https://github.com/Ombi-app/Ombi/commit/75906af0adee3e3c68d825c3aaa8f7b918461b1f)) * :bug: Fixed missing externals ([#4712](https://github.com/Ombi-app/Ombi/issues/4712)) ([fcc1eaa](https://github.com/Ombi-app/Ombi/commit/fcc1eaaa377683dcdc81d62a2a688fb0c4490c7b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc)) * 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] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb)) * **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))
* **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))
## [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 ### 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 ### 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 ### 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 ### 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 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) * **discover:** Fix cache mix up ([03d9422](https://github.com/Ombi-app/Ombi/commit/03d94220c7eaafb50c6c80a6ed1150794b873ac3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([356c742](https://github.com/Ombi-app/Ombi/commit/356c7424e0ce8c1c5063b04bc6ed9b809f214d65)) * **discover:** Fix new trending feature detection ([6794b88](https://github.com/Ombi-app/Ombi/commit/6794b887f6544fb41528bdd9728b7824b65e47ee))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6fcaecf](https://github.com/Ombi-app/Ombi/commit/6fcaecf80b766f2d43ac7082d74364238e1638b7)) * **settings:** Allow toggling features when there are more than one ([a373359](https://github.com/Ombi-app/Ombi/commit/a373359ae8e6bad42b558a6e01a8ff2840d3bbaa))
* **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))
### 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)) ## [4.16.12](https://github.com/Ombi-app/Ombi/compare/v4.16.11...v4.16.12) (2022-04-19)
* **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.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 ### 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)) * Set the default job for the watchlist import to hourly instead of daily ([75906af](https://github.com/Ombi-app/Ombi/commit/75906af0adee3e3c68d825c3aaa8f7b918461b1f))
* **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)) * **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc))
* **localisation:** Localize request types in notifications ([#4516](https://github.com/Ombi-app/Ombi/issues/4516)) ([e09435d](https://github.com/Ombi-app/Ombi/commit/e09435da455b12fc429f129372de31e0654da797)) * **translations:** 🌐 New translations from Crowdin [skip ci] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb))
* **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)) * **translations:** 🌐 New translations from Crowdin [skip ci] ([4fc2c1f](https://github.com/Ombi-app/Ombi/commit/4fc2c1f24534085a783a3d5791f5533b68272153))
* **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)) * **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)) ## [4.10.2](https://github.com/Ombi-app/Ombi/compare/v4.10.1...v4.10.2) (2022-01-22)
* **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.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 ### 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)) * **plex-watchlist:** Only request the latest season when importing from the watchlist ([77a47ff](https://github.com/Ombi-app/Ombi/commit/77a47ff157c6c5feafe3f2a29a3fcba8df4fdfef))
* **mediaserver:** fixed some more issues in the media server sync and availability checks ([f3ea979](https://github.com/Ombi-app/Ombi/commit/f3ea979b8bd77842780ce8e6928b16237dd779cf))
## [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 ### 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)) * **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))
* **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))
## [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 ### 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 ### 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 ### 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 ### 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> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/anojht"> <a href="https://github.com/sephrat">
<img src="https://avatars.githubusercontent.com/u/21053678?v=4" width="50;" alt="anojht"/> <img src="https://avatars.githubusercontent.com/u/34862846?v=4" width="50;" alt="sephrat"/>
<br /> <br />
<sub><b>Anojh Thayaparan</b></sub> <sub><b>Sephrat</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/sephrat"> <a href="https://github.com/anojht">
<img src="https://avatars.githubusercontent.com/u/34862846?v=4" width="50;" alt="sephrat"/> <img src="https://avatars.githubusercontent.com/u/21053678?v=4" width="50;" alt="anojht"/>
<br /> <br />
<sub><b>Sephrat</b></sub> <sub><b>Anojh Thayaparan</b></sub>
</a> </a>
</td></tr> </td></tr>
<tr> <tr>
@ -222,10 +222,10 @@ Here are some of the features Ombi has:
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/steffokeffo"> <a href="https://github.com/grimsan55">
<img src="https://avatars.githubusercontent.com/u/8499989?v=4" width="50;" alt="steffokeffo"/> <img src="https://avatars.githubusercontent.com/u/8499989?v=4" width="50;" alt="grimsan55"/>
<br /> <br />
<sub><b>Steffokeffo</b></sub> <sub><b>Stefan</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
@ -286,6 +286,13 @@ Here are some of the features Ombi has:
</a> </a>
</td></tr> </td></tr>
<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"> <td align="center">
<a href="https://github.com/mhann"> <a href="https://github.com/mhann">
<img src="https://avatars.githubusercontent.com/u/17162399?v=4" width="50;" alt="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 /> <br />
<sub><b>Austin Jackson</b></sub> <sub><b>Austin Jackson</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/D34DC3N73R"> <a href="https://github.com/D34DC3N73R">
<img src="https://avatars.githubusercontent.com/u/9123670?v=4" width="50;" alt="D34DC3N73R"/> <img src="https://avatars.githubusercontent.com/u/9123670?v=4" width="50;" alt="D34DC3N73R"/>
<br /> <br />
<sub><b>D34DC3N73R</b></sub> <sub><b>D34DC3N73R</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/pooley182"> <a href="https://github.com/pooley182">
<img src="https://avatars.githubusercontent.com/u/5040011?v=4" width="50;" alt="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 /> <br />
<sub><b>Jeffrey Peters</b></sub> <sub><b>Jeffrey Peters</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/MariusSchiffer"> <a href="https://github.com/MariusSchiffer">
<img src="https://avatars.githubusercontent.com/u/183124?v=4" width="50;" alt="MariusSchiffer"/> <img src="https://avatars.githubusercontent.com/u/183124?v=4" width="50;" alt="MariusSchiffer"/>
<br /> <br />
<sub><b>Marius Schiffer</b></sub> <sub><b>Marius Schiffer</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Qstick"> <a href="https://github.com/Qstick">
<img src="https://avatars.githubusercontent.com/u/376117?v=4" width="50;" alt="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 /> <br />
<sub><b>Abe Kline</b></sub> <sub><b>Abe Kline</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/XanderStrike"> <a href="https://github.com/XanderStrike">
<img src="https://avatars.githubusercontent.com/u/1565303?v=4" width="50;" alt="XanderStrike"/> <img src="https://avatars.githubusercontent.com/u/1565303?v=4" width="50;" alt="XanderStrike"/>
<br /> <br />
<sub><b>Alexander Standke</b></sub> <sub><b>Alexander Standke</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Alasano"> <a href="https://github.com/Alasano">
<img src="https://avatars.githubusercontent.com/u/14372930?v=4" width="50;" alt="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 /> <br />
<sub><b>Chris Lees</b></sub> <sub><b>Chris Lees</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/cdemi"> <a href="https://github.com/cdemi">
<img src="https://avatars.githubusercontent.com/u/8025435?v=4" width="50;" alt="cdemi"/> <img src="https://avatars.githubusercontent.com/u/8025435?v=4" width="50;" alt="cdemi"/>
<br /> <br />
<sub><b>Christopher Demicoli</b></sub> <sub><b>Christopher Demicoli</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Codehhh"> <a href="https://github.com/Codehhh">
<img src="https://avatars.githubusercontent.com/u/12055335?v=4" width="50;" alt="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 /> <br />
<sub><b>Devin Buhl</b></sub> <sub><b>Devin Buhl</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/elisspace"> <a href="https://github.com/elisspace">
<img src="https://avatars.githubusercontent.com/u/18365129?v=4" width="50;" alt="elisspace"/> <img src="https://avatars.githubusercontent.com/u/18365129?v=4" width="50;" alt="elisspace"/>
<br /> <br />
<sub><b>Eli</b></sub> <sub><b>Eli</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Fish2"> <a href="https://github.com/Fish2">
<img src="https://avatars.githubusercontent.com/u/2311734?v=4" width="50;" alt="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> <sub><b>Haries Ramdhani</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/ImgBotApp"> <a href="https://github.com/ImgBotApp">
<img src="https://avatars.githubusercontent.com/u/31427850?v=4" width="50;" alt="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 /> <br />
<sub><b>Jacob Pyke</b></sub> <sub><b>Jacob Pyke</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/jamesmacwhite"> <a href="https://github.com/jamesmacwhite">
<img src="https://avatars.githubusercontent.com/u/8067792?v=4" width="50;" alt="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 /> <br />
<sub><b>Joe Groocock</b></sub> <sub><b>Joe Groocock</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/errorhandler"> <a href="https://github.com/errorhandler">
<img src="https://avatars.githubusercontent.com/u/17112958?v=4" width="50;" alt="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 /> <br />
<sub><b>Kris Klosterman</b></sub> <sub><b>Kris Klosterman</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/kmlucy"> <a href="https://github.com/kmlucy">
<img src="https://avatars.githubusercontent.com/u/13952475?v=4" width="50;" alt="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 /> <br />
<sub><b>Lightkeeper</b></sub> <sub><b>Lightkeeper</b></sub>
</a> </a>
</td></tr> </td>
<tr> <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"> <td align="center">
<a href="https://github.com/devbymadde"> <a href="https://github.com/devbymadde">
<img src="https://avatars.githubusercontent.com/u/6094593?v=4" width="50;" alt="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> <sub><b>Madeleine Schönemann</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/mattmattmatt"> <a href="https://github.com/mattmattmatt">
<img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/> <img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/>
<br /> <br />
<sub><b>Matt</b></sub> <sub><b>Matt</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/LMaxence"> <a href="https://github.com/LMaxence">
<img src="https://avatars.githubusercontent.com/u/29194680?v=4" width="50;" alt="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 /> <br />
<sub><b>Qiming Chen</b></sub> <sub><b>Qiming Chen</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/randallbruder"> <a href="https://github.com/randallbruder">
<img src="https://avatars.githubusercontent.com/u/6447487?v=4" width="50;" alt="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 /> <br />
<sub><b>Rob Gökemeijer</b></sub> <sub><b>Rob Gökemeijer</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/sambartik"> <a href="https://github.com/sambartik">
<img src="https://avatars.githubusercontent.com/u/63553146?v=4" width="50;" alt="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> <sub><b>Shoghi</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/thomasvt1"> <a href="https://github.com/thomasvt1">
<img src="https://avatars.githubusercontent.com/u/2271011?v=4" width="50;" alt="thomasvt1"/> <img src="https://avatars.githubusercontent.com/u/2271011?v=4" width="50;" alt="thomasvt1"/>
<br /> <br />
<sub><b>Thomas Van Tilburg</b></sub> <sub><b>Thomas Van Tilburg</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Tim-Trott"> <a href="https://github.com/Tim-Trott">
<img src="https://avatars.githubusercontent.com/u/8249434?v=4" width="50;" alt="Tim-Trott"/> <img src="https://avatars.githubusercontent.com/u/8249434?v=4" width="50;" alt="Tim-Trott"/>
<br /> <br />
<sub><b>Tim Trott</b></sub> <sub><b>Tim Trott</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/tombomb"> <a href="https://github.com/tombomb">
<img src="https://avatars.githubusercontent.com/u/544509?v=4" width="50;" alt="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 /> <br />
<sub><b>Tim OBrien</b></sub> <sub><b>Tim OBrien</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/x-limitless-x"> <a href="https://github.com/x-limitless-x">
<img src="https://avatars.githubusercontent.com/u/17127926?v=4" width="50;" alt="x-limitless-x"/> <img src="https://avatars.githubusercontent.com/u/17127926?v=4" width="50;" alt="x-limitless-x"/>
<br /> <br />
<sub><b>Blake Drumm</b></sub> <sub><b>Blake Drumm</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/camjac251"> <a href="https://github.com/camjac251">
<img src="https://avatars.githubusercontent.com/u/6313132?v=4" width="50;" alt="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> <sub><b>Dorian ALKOUM</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/m4tta"> <a href="https://github.com/m4tta">
<img src="https://avatars.githubusercontent.com/u/427218?v=4" width="50;" alt="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> <sub><b>Patrick Weber</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/sir-marv"> <a href="https://github.com/sir-marv">
<img src="https://avatars.githubusercontent.com/u/3598205?v=4" width="50;" alt="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("Fields", "ProviderIds,Overview");
request.AddQueryString("IsVirtualItem", "False"); request.AddQueryString("IsMissing", "False");
return await Api.Request<EmbyItemContainer<EmbyMovie>>(request); return await Api.Request<EmbyItemContainer<EmbyMovie>>(request);
} }
@ -180,7 +180,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("ParentId", parentIdFilder); request.AddQueryString("ParentId", parentIdFilder);
} }
request.AddQueryString("IsVirtualItem", "False"); request.AddQueryString("IsMissing", "False");
AddHeaders(request, apiKey); AddHeaders(request, apiKey);
@ -207,7 +207,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("IncludeItemTypes", type); request.AddQueryString("IncludeItemTypes", type);
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("IsVirtualItem", "False"); request.AddQueryString("IsMissing", "False");
AddHeaders(request, apiKey); AddHeaders(request, apiKey);
@ -229,7 +229,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("ParentId", parentIdFilder); request.AddQueryString("ParentId", parentIdFilder);
} }
request.AddQueryString("IsVirtualItem", "False"); request.AddQueryString("isMissing", "False");
AddHeaders(request, apiKey); AddHeaders(request, apiKey);

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

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

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

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

@ -1,6 +1,8 @@
using System.Net.Http; using System.Net.Http;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models;
using Ombi.Api.Sonarr.Models.V3; using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr namespace Ombi.Api.Sonarr
@ -21,5 +23,12 @@ namespace Ombi.Api.Sonarr
return await Api.Request<List<LanguageProfiles>>(request); 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, Status = movieInfo.Status,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.Now, RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.Now,
Approved = false, Approved = false,
Approved4K = false,
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id, RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
Background = movieInfo.BackdropPath, Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode, LangCode = model.LanguageCode,
@ -151,7 +152,7 @@ namespace Ombi.Core.Engine
var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US"); var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
requestModel.DigitalReleaseDate = usDates?.ReleaseDate requestModel.DigitalReleaseDate = usDates?.ReleaseDate
?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate; ?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
var ruleResults = (await RunRequestRules(requestModel)).ToList(); var ruleResults = (await RunRequestRules(requestModel)).ToList();
var ruleResultInError = ruleResults.Find(x => !x.Success); var ruleResultInError = ruleResults.Find(x => !x.Success);
if (ruleResultInError != null) if (ruleResultInError != null)
@ -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); var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf, isExisting, is4kRequest);
if (requestEngineResult.Result) if (requestEngineResult.Result)
@ -333,7 +334,7 @@ namespace Ombi.Core.Engine
allRequests = allRequests.Where(x => allRequests = allRequests.Where(x =>
(x.RequestedDate != DateTime.MinValue && x.Approved && !x.Available && (!x.Denied.HasValue || !x.Denied.Value)) (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; break;
case RequestStatus.Available: case RequestStatus.Available:
@ -533,7 +534,10 @@ namespace Ombi.Core.Engine
} }
else else
{ {
x.ShowSubscribe = true; if (!x.Available && !x.Available4K && (!x.Denied ?? true) && (!x.Denied4K ?? true))
{
x.ShowSubscribe = true;
}
var hasSub = sub.FirstOrDefault(r => r.RequestId == x.Id); var hasSub = sub.FirstOrDefault(r => r.RequestId == x.Id);
x.Subscribed = hasSub != null; x.Subscribed = hasSub != null;
} }

@ -16,7 +16,6 @@ using Ombi.Store.Repository;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
@ -161,7 +160,7 @@ namespace Ombi.Core.Engine
var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode); return await MovieApi.UpcomingMovies(langCode);
}, DateTimeOffset.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
@ -216,34 +215,9 @@ namespace Ombi.Core.Engine
await RunSearchRules(viewMovie); await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
return 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) private async Task<SearchMovieViewModel> ProcessSingleMovie(MovieDbSearchResult movie)
{ {

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

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

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

@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Models.Search.V2; using Ombi.Core.Models.Search.V2;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in // Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models // 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<List<MultiSearchResult>> MultiSearch(string searchTerm, MultiSearchFilter filter, CancellationToken cancellationToken);
Task<IEnumerable<Genre>> GetGenres(string media, CancellationToken requestAborted); 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.Search.V2;
using Ombi.Core.Models.UI; using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Services;
using Ombi.Core.Settings; using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
@ -31,7 +32,8 @@ namespace Ombi.Core.Engine.V2
{ {
public MovieSearchEngineV2(ICurrentUser identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, 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, 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) : base(identity, service, r, um, mem, s, sub)
{ {
MovieApi = movApi; MovieApi = movApi;
@ -40,6 +42,7 @@ namespace Ombi.Core.Engine.V2
_customizationSettings = customizationSettings; _customizationSettings = customizationSettings;
_movieRequestEngine = movieRequestEngine; _movieRequestEngine = movieRequestEngine;
_client = httpClientFactory.CreateClient(); _client = httpClientFactory.CreateClient();
_feature = feature;
} }
private IMovieDbApi MovieApi { get; } private IMovieDbApi MovieApi { get; }
@ -48,6 +51,7 @@ namespace Ombi.Core.Engine.V2
private readonly ISettingsService<CustomizationSettings> _customizationSettings; private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly IMovieRequestEngine _movieRequestEngine; private readonly IMovieRequestEngine _movieRequestEngine;
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly IFeatureService _feature;
public async Task<MovieFullInfoViewModel> GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null) 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 langCode = await DefaultLanguageCode(null);
//var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems);
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
//foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
//{ {
var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken); var apiResult = await MovieApi.AdvancedSearch(model, pagesToLoad.Page, cancellationToken);
//results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
//} }
return await TransformMovieResultsToResponse(apiResult); return await TransformMovieResultsToResponse(results);
} }
/// <summary> /// <summary>
@ -196,14 +200,19 @@ namespace Ombi.Core.Engine.V2
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies(int currentPosition, int amountToLoad) public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies(int currentPosition, int amountToLoad)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
var isOldTrendingSourceEnabled = await _feature.FeatureEnabled(FeatureNames.OldTrendingSource);
var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems);
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) 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, 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)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results); return await TransformMovieResultsToResponse(results);
@ -278,7 +287,7 @@ namespace Ombi.Core.Engine.V2
var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode); return await MovieApi.UpcomingMovies(langCode);
}, DateTimeOffset.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
@ -298,7 +307,7 @@ namespace Ombi.Core.Engine.V2
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAddAsync(nameof(UpcomingMovies) + pagesToLoad.Page + langCode, 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)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results); return await TransformMovieResultsToResponse(results);
@ -393,8 +402,6 @@ namespace Ombi.Core.Engine.V2
await RunSearchRules(viewMovie); 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); var mapped = Mapper.Map<MovieFullInfoViewModel>(movie);
mapped.Available = viewMovie.Available; mapped.Available = viewMovie.Available;
@ -406,8 +413,6 @@ namespace Ombi.Core.Engine.V2
mapped.PlexUrl = viewMovie.PlexUrl; mapped.PlexUrl = viewMovie.PlexUrl;
mapped.EmbyUrl = viewMovie.EmbyUrl; mapped.EmbyUrl = viewMovie.EmbyUrl;
mapped.JellyfinUrl = viewMovie.JellyfinUrl; mapped.JellyfinUrl = viewMovie.JellyfinUrl;
mapped.Subscribed = viewMovie.Subscribed;
mapped.ShowSubscribe = viewMovie.ShowSubscribe;
mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate; mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate;
mapped.RequestedDate4k = viewMovie.RequestedDate4k; mapped.RequestedDate4k = viewMovie.RequestedDate4k;
mapped.Approved4K = viewMovie.Approved4K; mapped.Approved4K = viewMovie.Approved4K;
@ -429,8 +434,6 @@ namespace Ombi.Core.Engine.V2
var mappedMovie = Mapper.Map<SearchMovieViewModel>(movie); var mappedMovie = Mapper.Map<SearchMovieViewModel>(movie);
await RunSearchRules(mappedMovie); 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); var mapped = Mapper.Map<MovieCollection>(movie);
mapped.Available = movie.Available; mapped.Available = movie.Available;
@ -440,8 +443,6 @@ namespace Ombi.Core.Engine.V2
mapped.PlexUrl = movie.PlexUrl; mapped.PlexUrl = movie.PlexUrl;
mapped.EmbyUrl = movie.EmbyUrl; mapped.EmbyUrl = movie.EmbyUrl;
mapped.JellyfinUrl = movie.JellyfinUrl; mapped.JellyfinUrl = movie.JellyfinUrl;
mapped.Subscribed = movie.Subscribed;
mapped.ShowSubscribe = movie.ShowSubscribe;
mapped.ReleaseDate = movie.ReleaseDate; mapped.ReleaseDate = movie.ReleaseDate;
} }
return viewMovie; return viewMovie;
@ -470,34 +471,9 @@ namespace Ombi.Core.Engine.V2
await RunSearchRules(viewMovie); await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
return 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) 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.Settings.Settings.Models.External;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in // Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models // Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
@ -124,5 +125,9 @@ namespace Ombi.Core.Engine.V2
var lang = await DefaultLanguageCode(null); var lang = await DefaultLanguageCode(null);
return await _movieDbApi.GetGenres(media, cancellationToken, lang); 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.Engine.Interfaces;
using Ombi.Core.Models.UI; using Ombi.Core.Models.UI;
using Ombi.Core.Helpers; using Ombi.Core.Helpers;
using Ombi.Core.Services;
namespace Ombi.Core.Engine.V2 namespace Ombi.Core.Engine.V2
{ {
@ -37,10 +38,12 @@ namespace Ombi.Core.Engine.V2
private readonly IMovieDbApi _movieApi; private readonly IMovieDbApi _movieApi;
private readonly ISettingsService<CustomizationSettings> _customization; private readonly ISettingsService<CustomizationSettings> _customization;
private readonly ITvRequestEngine _requestEngine; private readonly ITvRequestEngine _requestEngine;
private readonly IFeatureService _feature;
public TvSearchEngineV2(ICurrentUser identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, public TvSearchEngineV2(ICurrentUser identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s, 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) : base(identity, service, r, um, memCache, s, sub)
{ {
_tvMaze = tvMaze; _tvMaze = tvMaze;
@ -49,6 +52,7 @@ namespace Ombi.Core.Engine.V2
_movieApi = movieApi; _movieApi = movieApi;
_customization = customization; _customization = customization;
_requestEngine = requestEngine; _requestEngine = requestEngine;
_feature = feature;
} }
@ -132,15 +136,19 @@ namespace Ombi.Core.Engine.V2
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad) public async Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
var isOldTrendingSourceEnabled = await _feature.FeatureEnabled(FeatureNames.OldTrendingSource);
var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit);
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) 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, 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)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
@ -264,11 +272,17 @@ namespace Ombi.Core.Engine.V2
Overview = tvSeason.overview, Overview = tvSeason.overview,
Episodes = new List<EpisodeRequests>() 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 newSeason.Episodes.Add(new EpisodeRequests
{ {
//Url = episode...ToHttpsUrl(), //Url = episode...ToHttpsUrl(),
Title = episode.name, Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue, AirDate = airDate,
EpisodeNumber = episode.episode_number, EpisodeNumber = episode.episode_number,
}); });
@ -276,12 +290,18 @@ namespace Ombi.Core.Engine.V2
} }
else 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 // We already have the season, so just add the episode
season.Episodes.Add(new EpisodeRequests season.Episodes.Add(new EpisodeRequests
{ {
//Url = e.url.ToHttpsUrl(), //Url = e.url.ToHttpsUrl(),
Title = episode.name, Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue, AirDate = airDate,
EpisodeNumber = episode.episode_number, EpisodeNumber = episode.episode_number,
}); });
} }

@ -261,9 +261,16 @@ namespace Ombi.Core.Helpers
return this; 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.Entities.Requests;
using Ombi.Store.Repository.Requests; using Ombi.Store.Repository.Requests;
using System.Threading; using System.Threading;
using Ombi.Helpers;
namespace Ombi.Core.Helpers namespace Ombi.Core.Helpers
{ {
@ -255,9 +256,16 @@ namespace Ombi.Core.Helpers
return this; 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")] [Obsolete("Use request service instead")]
public bool Subscribed { get; set; } public bool Subscribed { get; set; }
[NotMapped] [NotMapped]
[Obsolete("Use request service instead")]
public bool ShowSubscribe { get; set; } 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) private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)

@ -133,8 +133,7 @@ namespace Ombi.Core.Senders
return new SenderResult return new SenderResult
{ {
Success = false, Success = false
Message = "Something went wrong!"
}; };
} }
@ -343,8 +342,6 @@ namespace Ombi.Core.Senders
await Task.Delay(500); await Task.Delay(500);
} }
var seriesChanges = false;
foreach (var season in model.SeasonRequests) foreach (var season in model.SeasonRequests)
{ {
foreach (var ep in season.Episodes) foreach (var ep in season.Episodes)
@ -359,72 +356,36 @@ namespace Ombi.Core.Senders
} }
existingSeason = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); 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);
// Make sure this season is set to monitored
if (sonarrEpCount == ourRequestCount /*|| !missingEpisodes.Any()*/) if (!existingSeason.monitored)
{ {
// We have the same amount of requests as all of the episodes in the season. // 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
existingSeason.monitored = true; existingSeason.monitored = true;
seriesChanges = true; var sea = result.seasons.FirstOrDefault(x => x.seasonNumber == existingSeason.seasonNumber);
sea.monitored = true;
// We do not need to update the episodes as marking the season as monitored will mark the episodes as monitored. result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
var seasonToUpdate = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); var epToUnmonitored = new List<Episode>();
seasonToUpdate.monitored = true; // Update by ref 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())
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 ep.monitored = false;
// So we need to monitor the series but unmonitor every episode epToUnmonitored.Add(ep);
// 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);
}
foreach (var epToUpdate in epToUnmonitored)
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
} }
// Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber)) foreach (var epToUpdate in epToUnmonitored)
{ {
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri); await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
} }
} }
} // Now update the episodes that need updating
if (seriesChanges) foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber))
{ {
await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result); await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
} }
if (!s.AddOnly) if (!s.AddOnly)

@ -83,6 +83,8 @@ namespace Ombi.Helpers.Tests
.SetName("PaginationPosition_Load_SecondHalf_FirstPage_FirstHalf_SecondPage"); .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) }) 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"); .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) }) 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"); .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) }) 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 lastPage = lastItemIndex / maxItemsPerPage + 1;
var stopPos = lastItemIndex % maxItemsPerPage + 1; var stopPos = lastItemIndex % maxItemsPerPage + 1;
while (currentlyLoaded > maxItemsPerPage) while (currentlyLoaded >= maxItemsPerPage)
{ {
currentlyLoaded -= maxItemsPerPage; currentlyLoaded -= maxItemsPerPage;
} }

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

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

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

@ -157,9 +157,20 @@ namespace Ombi.Schedule.Jobs.Emby
} }
var existingTv = await _repo.GetByEmbyId(tvShow.Id); 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) if (existingTv == null)
{ {
_logger.LogDebug("Adding new TV Show {0}", tvShow.Name); _logger.LogDebug("Adding TV Show {0}", tvShow.Name);
mediaToAdd.Add(new EmbyContent mediaToAdd.Add(new EmbyContent
{ {
TvDbId = tvShow.ProviderIds?.Tvdb, TvDbId = tvShow.ProviderIds?.Tvdb,
@ -265,23 +276,21 @@ namespace Ombi.Schedule.Jobs.Emby
return; return;
} }
_logger.LogDebug($"Adding new movie {movieInfo.Name}"); _logger.LogDebug($"Adding new movie {movieInfo.Name}");
var newMovie = new EmbyContent();
content.Add(new EmbyContent newMovie.AddedAt = DateTime.UtcNow;
{ MapEmbyContent(newMovie, movieInfo, server, has4K, quality);
ImdbId = movieInfo.ProviderIds.Imdb, content.Add(newMovie);
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
});
} }
else 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}'"); _logger.LogDebug($"We have found another quality for Movie '{movieInfo.Name}', Quality: '{quality}'");
existingMovie.Quality = has4K ? null : quality; existingMovie.Quality = has4K ? null : quality;
@ -290,6 +299,11 @@ namespace Ombi.Schedule.Jobs.Emby
// Probably could refactor here // Probably could refactor here
// If a 4k movie comes in (we don't store the quality on 4k) // 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 // it will always get updated even know it's not changed
movieHasChanged = true;
}
if (movieHasChanged)
{
toUpdate.Add(existingMovie); toUpdate.Add(existingMovie);
} }
else 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) private bool ValidateSettings(EmbyServers server)
{ {
if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey))

@ -130,12 +130,6 @@ namespace Ombi.Schedule.Jobs.Emby
{ {
processed++; 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, // Let's make sure we have the parent request, stop those pesky forign key errors,
// Damn me having data integrity // Damn me having data integrity
var parent = await _repo.GetByEmbyId(ep.SeriesId); var parent = await _repo.GetByEmbyId(ep.SeriesId);
@ -176,18 +170,26 @@ namespace Ombi.Schedule.Jobs.Emby
if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber)
{ {
epToAdd.Add(new EmbyEpisode int episodeNumber = ep.IndexNumber;
do
{ {
EmbyId = ep.Id, _logger.LogDebug($"Multiple-episode file detected. Adding episode ${episodeNumber}");
EpisodeNumber = ep.IndexNumberEnd.Value, episodeNumber++;
SeasonNumber = ep.ParentIndexNumber, epToAdd.Add(new EmbyEpisode
ParentId = ep.SeriesId, {
TvDbId = ep.ProviderIds.Tvdb, EmbyId = ep.Id,
TheMovieDbId = ep.ProviderIds.Tmdb, EpisodeNumber = episodeNumber,
ImdbId = ep.ProviderIds.Imdb, SeasonNumber = ep.ParentIndexNumber,
Title = ep.Name, ParentId = ep.SeriesId,
AddedAt = DateTime.UtcNow TvDbId = ep.ProviderIds.Tvdb,
}); TheMovieDbId = ep.ProviderIds.Tmdb,
ImdbId = ep.ProviderIds.Imdb,
Title = ep.Name,
AddedAt = DateTime.UtcNow
});
} while (episodeNumber < ep.IndexNumberEnd.Value);
} }
} }
} }

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

@ -132,9 +132,20 @@ namespace Ombi.Schedule.Jobs.Jellyfin
} }
var existingTv = await _repo.GetByJellyfinId(tvShow.Id); 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) if (existingTv == null)
{ {
_logger.LogDebug("Adding new TV Show {0}", tvShow.Name); _logger.LogDebug("Adding TV Show {0}", tvShow.Name);
mediaToAdd.Add(new JellyfinContent mediaToAdd.Add(new JellyfinContent
{ {
TvDbId = tvShow.ProviderIds?.Tvdb, TvDbId = tvShow.ProviderIds?.Tvdb,
@ -230,22 +241,21 @@ namespace Ombi.Schedule.Jobs.Jellyfin
return; return;
} }
_logger.LogDebug($"Adding new movie {movieInfo.Name}"); _logger.LogDebug($"Adding new movie {movieInfo.Name}");
content.Add(new JellyfinContent var newMovie = new JellyfinContent();
{ newMovie.AddedAt = DateTime.UtcNow;
ImdbId = movieInfo.ProviderIds.Imdb, MapJellyfinMovie(newMovie, movieInfo, server, has4K, quality);
TheMovieDbId = movieInfo.ProviderIds?.Tmdb, content.Add(newMovie);;
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
});
} }
else 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}'"); _logger.LogDebug($"We have found another quality for Movie '{movieInfo.Name}', Quality: '{quality}'");
existingMovie.Quality = has4K ? null : 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) // 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 // it will always get updated even know it's not changed
toUpdate.Add(existingMovie); toUpdate.Add(existingMovie);
movieHasChanged = true;
}
if (movieHasChanged)
{
toUpdate.Add(existingMovie);
} }
else 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) private bool ValidateSettings(JellyfinServers server)
{ {
if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey))

@ -106,12 +106,6 @@ namespace Ombi.Schedule.Jobs.Jellyfin
{ {
processed++; 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, // Let's make sure we have the parent request, stop those pesky forign key errors,
// Damn me having data integrity // Damn me having data integrity
var parent = await _repo.GetByJellyfinId(ep.SeriesId); var parent = await _repo.GetByJellyfinId(ep.SeriesId);
@ -152,18 +146,25 @@ namespace Ombi.Schedule.Jobs.Jellyfin
if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber)
{ {
epToAdd.Add(new JellyfinEpisode int episodeNumber = ep.IndexNumber;
do
{ {
JellyfinId = ep.Id, _logger.LogDebug($"Multiple-episode file detected. Adding episode ${episodeNumber}");
EpisodeNumber = ep.IndexNumberEnd.Value, episodeNumber++;
SeasonNumber = ep.ParentIndexNumber, epToAdd.Add(new JellyfinEpisode
ParentId = ep.SeriesId, {
TvDbId = ep.ProviderIds.Tvdb, JellyfinId = ep.Id,
TheMovieDbId = ep.ProviderIds.Tmdb, EpisodeNumber = episodeNumber,
ImdbId = ep.ProviderIds.Imdb, SeasonNumber = ep.ParentIndexNumber,
Title = ep.Name, ParentId = ep.SeriesId,
AddedAt = DateTime.UtcNow TvDbId = ep.ProviderIds.Tvdb,
}); TheMovieDbId = ep.ProviderIds.Tmdb,
ImdbId = ep.ProviderIds.Imdb,
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) private HashSet<IMediaServerEpisode> FilterEpisodes(IEnumerable<IMediaServerEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{ {
var itemsToReturn = new HashSet<IMediaServerEpisode>(); 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); var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId);
if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber)) 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) foreach (var content in ordered)
{ {
int.TryParse(content.TheMovieDbId, out var movieDbId); 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 info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId, defaultLanguageCode);
var mediaurl = content.Url; var mediaurl = content.Url;
if (info == null) 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; continue;
} }
try try
@ -665,48 +662,18 @@ namespace Ombi.Schedule.Jobs.Ombi
var orderedTv = series.OrderByDescending(x => x.AddedAt); var orderedTv = series.OrderByDescending(x => x.AddedAt);
foreach (var t in orderedTv) 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 try
{ {
var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId, languageCode); var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId, languageCode);
if (tvInfo == null) 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; continue;
} }
if (tvInfo.backdrop_path.HasValue()) if (tvInfo.backdrop_path.HasValue())
{ {
AddBackgroundInsideTable($"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}"); AddBackgroundInsideTable($"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}");
} }
else else
@ -732,7 +699,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
catch (Exception e) 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 finally
{ {

@ -11,5 +11,7 @@ namespace Ombi.Core.Settings.Models.External
public List<int> ExcludedMovieGenreIds { get; set; } public List<int> ExcludedMovieGenreIds { get; set; }
public List<int> ExcludedTvGenreIds { 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 static class FeatureNames
{ {
public const string Movie4KRequests = nameof(Movie4KRequests); public const string Movie4KRequests = nameof(Movie4KRequests);
public const string OldTrendingSource = nameof(OldTrendingSource);
} }
} }

@ -212,7 +212,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, 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!", Subject = "{ApplicationName}: Partially Available Request!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,

@ -102,6 +102,13 @@ namespace Ombi.Store.Repository
return InternalSaveChanges(); 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; public override RecentlyAddedType RecentlyAddedType => RecentlyAddedType.Emby;
} }
} }

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

@ -104,6 +104,13 @@ namespace Ombi.Store.Repository
return InternalSaveChanges(); 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; public override RecentlyAddedType RecentlyAddedType => RecentlyAddedType.Jellyfin;
} }
} }

@ -22,5 +22,6 @@ namespace Ombi.Store.Repository
public abstract Task AddRange(IEnumerable<IMediaServerEpisode> content); public abstract Task AddRange(IEnumerable<IMediaServerEpisode> content);
public abstract void UpdateWithoutSave(IMediaServerContent existingContent); public abstract void UpdateWithoutSave(IMediaServerContent existingContent);
public abstract Task UpdateRange(IEnumerable<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); Db.PlexServerContent.UpdateRange((IEnumerable<PlexServerContent>)existingContent);
return InternalSaveChanges(); 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> GetMovieInformation(int movieId);
Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId, string langCode = "en"); Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId, string langCode = "en");
Task<List<MovieDbSearchResult>> NowPlaying(string languageCode, int? page = null); 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>> 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>> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken));
Task<List<MovieDbSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode); 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<MovieDbSearchResult>> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null);
Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default); Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default);
Task<List<MovieDbSearchResult>> TopRated(string languageCode, int? page = null); 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>> 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>> UpcomingTv(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> SimilarMovies(int movieId, string langCode); Task<List<MovieDbSearchResult>> SimilarMovies(int movieId, string langCode);
Task<FindResult> Find(string externalId, ExternalSource source); Task<FindResult> Find(string externalId, ExternalSource source);
@ -41,7 +43,8 @@ namespace Ombi.Api.TheMovieDb
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token); Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token); Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
Task<List<Genre>> GetGenres(string media, CancellationToken cancellationToken, string languageCode); 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<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,11 +70,11 @@ 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); var request = new Request($"discover/{model.Type}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
if(model.ReleaseYear.HasValue && model.ReleaseYear.Value > 1900) if (model.ReleaseYear.HasValue && model.ReleaseYear.Value > 1900)
{ {
request.FullUri = request.FullUri.AddQueryParameter("year", model.ReleaseYear.Value.ToString()); request.FullUri = request.FullUri.AddQueryParameter("year", model.ReleaseYear.Value.ToString());
} }
@ -92,6 +92,9 @@ namespace Ombi.Api.TheMovieDb
} }
//request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); //request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc");
request.AddQueryString("page", page.ToString());
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request, cancellationToken); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results); return Mapper.Map<List<MovieDbSearchResult>>(result.results);
} }
@ -139,7 +142,7 @@ namespace Ombi.Api.TheMovieDb
var result = await Api.Request<ActorCredits>(request); var result = await Api.Request<ActorCredits>(request);
return result; return result;
} }
public async Task<ActorCredits> GetActorTvCredits(int actorId, string langCode) public async Task<ActorCredits> GetActorTvCredits(int actorId, string langCode)
{ {
var request = new Request($"person/{actorId}/tv_credits", BaseUri, HttpMethod.Get); var request = new Request($"person/{actorId}/tv_credits", BaseUri, HttpMethod.Get);
@ -282,21 +285,38 @@ namespace Ombi.Api.TheMovieDb
return Mapper.Map<List<MovieDbSearchResult>>(result.results); 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> /// <remarks>
/// Maintains filter parity with <a href="https://developers.themoviedb.org/3/movies/get-upcoming">/movie/upcoming</a>. /// Maintains filter parity with <a href="https://developers.themoviedb.org/3/movies/get-upcoming">/movie/upcoming</a>.
/// </remarks> /// </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("api_key", ApiToken);
request.AddQueryString("language", langCode); request.AddQueryString("language", langCode);
@ -313,7 +333,27 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("page", page.ToString()); request.AddQueryString("page", page.ToString());
} }
await AddDiscoverSettings(request); 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); AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results); return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -376,8 +416,8 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("language", langCode); request.AddQueryString("language", langCode);
request.AddQueryString("sort_by", "vote_average.desc"); request.AddQueryString("sort_by", "vote_average.desc");
request.AddQueryString("with_keywords", keywordId); request.AddQueryString("with_keywords", keywordId);
// `vote_count` consideration isn't explicitly documented, but using only the `sort_by` filter // `vote_count` consideration isn't explicitly documented, but using only the `sort_by` filter
// does not provide the same results as `/movie/top_rated`. This appears to be adequate enough // does not provide the same results as `/movie/top_rated`. This appears to be adequate enough
// to filter out extremely high-rated movies due to very little votes // to filter out extremely high-rated movies due to very little votes
@ -438,6 +478,16 @@ namespace Ombi.Api.TheMovieDb
return result.genres ?? new List<Genre>(); 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) public Task<TheMovieDbContainer<MultiSearch>> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken)
{ {
var request = new Request("search/multi", BaseUri, HttpMethod.Get); 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)); 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) private async Task AddGenreFilter(Request request, string media_type)
@ -479,7 +533,8 @@ namespace Ombi.Api.TheMovieDb
var settings = await Settings; var settings = await Settings;
List<int> excludedGenres; List<int> excludedGenres;
switch (media_type) { switch (media_type)
{
case "tv": case "tv":
excludedGenres = settings.ExcludedTvGenreIds; excludedGenres = settings.ExcludedTvGenreIds;
break; break;

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

@ -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", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json", "tsConfig": "src/tsconfig.json",
"assets": [ "assets": [
"src/assets" "src/assets"
], ],
@ -114,7 +114,7 @@
"builder": "@angular-devkit/build-angular:tslint", "builder": "@angular-devkit/build-angular:tslint",
"options": { "options": {
"tsConfig": [ "tsConfig": [
"src/tsconfig.app.json" "src/tsconfig.json"
], ],
"exclude": [ "exclude": [
"**/node_modules/**" "**/node_modules/**"
@ -124,7 +124,6 @@
} }
} }
}, },
"defaultProject": "ombi",
"cli": { "cli": {
"analytics": false "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": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --port 3578 --configuration hmr", "start": "ng serve --port 3578 --configuration hmr",
"build": "node --max_old_space_size=6144 node_modules/@angular/cli/bin/ng build --prod", "build": "node --max_old_space_size=6144 node_modules/@angular/cli/bin/ng build -c production",
"lint": "ng lint" "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, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^13.2.0", "@angular/animations": "^14.0.0",
"@angular/cdk": "^13.2.0", "@angular/cdk": "^13.2.0",
"@angular/common": "^13.2.0", "@angular/common": "^14.0.0",
"@angular/compiler": "^13.2.0", "@angular/compiler": "^14.0.0",
"@angular/core": "^13.2.0", "@angular/core": "^14.0.0",
"@angular/forms": "^13.2.0", "@angular/forms": "^14.0.0",
"@angular/localize": "^13.2.0", "@angular/localize": "^14.0.0",
"@angular/material": "^13.2.0", "@angular/material": "^13.2.0",
"@angular/platform-browser": "^13.2.0", "@angular/platform-browser": "^14.0.0",
"@angular/platform-browser-dynamic": "^13.2.0", "@angular/platform-browser-dynamic": "^14.0.0",
"@angular/platform-server": "^13.2.0", "@angular/platform-server": "^14.0.0",
"@angular/router": "^13.2.0", "@angular/router": "^14.0.0",
"@angularclass/hmr": "^3.0.0", "@angularclass/hmr": "^3.0.0",
"@aspnet/signalr": "^1.1.0",
"@auth0/angular-jwt": "^5.0.2", "@auth0/angular-jwt": "^5.0.2",
"@fortawesome/fontawesome-free": "^6.0.0", "@fortawesome/fontawesome-free": "^6.0.0",
"@fullcalendar/core": "^4.2.0", "@fullcalendar/core": "^4.2.0",
"@fullcalendar/daygrid": "^4.4.0", "@fullcalendar/daygrid": "^4.4.0",
"@fullcalendar/interaction": "^4.2.0", "@fullcalendar/interaction": "^4.2.0",
"@microsoft/signalr": "^6.0.7",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0", "@ngx-translate/http-loader": "^7.0.0",
"@ngxs/devtools-plugin": "^3.7.3", "@ngxs/devtools-plugin": "^3.7.3",
"@ngxs/store": "^3.7.3", "@ngxs/store": "^3.7.3",
"@types/jquery": "^3.5.13", "@types/jquery": "^3.5.13",
"@yellowspot/ng-truncate": "^2.0.0", "@yellowspot/ng-truncate": "^2.0.0",
"angular-bootstrap-md": "^7.5.4",
"angular-router-loader": "^0.8.5", "angular-router-loader": "^0.8.5",
"angularx-qrcode": "^13.0.3", "angularx-qrcode": "^13.0.3",
"bootstrap": "^4.2.1", "bootstrap": "^4.2.1",
@ -48,34 +51,52 @@
"moment": "^2.29.1", "moment": "^2.29.1",
"ng2-cookies": "^1.0.12", "ng2-cookies": "^1.0.12",
"ngx-clipboard": "^12.1.0", "ngx-clipboard": "^12.1.0",
"ngx-infinite-scroll": "^9.0.0", "ngx-infinite-scroll": "^14.0.0",
"ngx-moment": "^3.0.1", "ngx-moment": "^3.0.1",
"ngx-order-pipe": "^2.2.0", "ngx-order-pipe": "^2.2.0",
"popper.js": "^1.14.3", "popper.js": "^1.14.3",
"primeicons": "^5.0.0", "primeicons": "^5.0.0",
"primeng": "^13.2.0", "primeng": "^13.2.0",
"protractor": "~5.4.0",
"rxjs": "^7.5.4", "rxjs": "^7.5.4",
"sass-recursive-map-merge": "^1.0.1", "sass-recursive-map-merge": "^1.0.1",
"store": "^2.0.12", "store": "^2.0.12",
"ts-md5": "^1.2.7", "ts-md5": "^1.2.7",
"ts-node": "~5.0.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"tslint": "^5.12.0",
"tslint-angular": "^1.1.2", "tslint-angular": "^1.1.2",
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~13.2.0", "@angular-devkit/build-angular": "^14.0.0",
"@angular/cli": "~13.2.0", "@angular/cli": "^14.0.0",
"@angular/compiler-cli": "^13.2.0", "@angular/compiler-cli": "^14.0.0",
"@angular/language-service": "^13.2.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/jasmine": "~3.6.7",
"@types/jasminewd2": "~2.0.8", "@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", "codelyzer": "^6.0.1",
"typescript": "~4.5.5" "typescript": "~4.7.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"protractor": "~5.4.0", "protractor": "~5.4.0",
"ts-node": "~5.0.1", "ts-node": "~5.0.1",
"tslint": "^5.12.0" "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 { APP_BASE_HREF, CommonModule, PlatformLocation } from "@angular/common";
import { CardsFreeModule, MDBBootstrapModule, NavbarModule } from "angular-bootstrap-md";
import { CustomPageService, ImageService, RequestService, SettingsService } from "./services"; import { CustomPageService, ImageService, RequestService, SettingsService } from "./services";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http"; 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 { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor"; import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor";
import { ImageBackgroundComponent, ImageComponent } from "./components/";
import { environment } from "../environments/environment"; import { environment } from "../environments/environment";
const routes: Routes = [ const routes: Routes = [
@ -131,7 +131,6 @@ export function JwtTokenGetter() {
MatSnackBarModule, MatSnackBarModule,
DialogModule, DialogModule,
MatButtonModule, MatButtonModule,
NavbarModule,
MatCardModule, MatCardModule,
MatTooltipModule, MatTooltipModule,
MatMenuModule, MatMenuModule,
@ -145,11 +144,9 @@ export function JwtTokenGetter() {
ConfirmDialogModule, ConfirmDialogModule,
OverlayPanelModule, OverlayPanelModule,
CommonModule, CommonModule,
CardsFreeModule,
OverlayModule, OverlayModule,
MatCheckboxModule, MatCheckboxModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(),
JwtModule.forRoot({ JwtModule.forRoot({
config: { config: {
tokenGetter: JwtTokenGetter, tokenGetter: JwtTokenGetter,
@ -170,7 +167,9 @@ export function JwtTokenGetter() {
...environment.production ? [] : ...environment.production ? [] :
[ [
NgxsReduxDevtoolsPluginModule.forRoot(), NgxsReduxDevtoolsPluginModule.forRoot(),
] ],
ImageBackgroundComponent,
ImageComponent,
], ],
declarations: [ declarations: [
AppComponent, 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 { 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 { DomSanitizer } from "@angular/platform-browser";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { CustomPageService, NotificationService } from "../services"; import { CustomPageService, NotificationService } from "../services";
@ -10,11 +10,11 @@ import { CustomPageService, NotificationService } from "../services";
}) })
export class CustomPageComponent implements OnInit { export class CustomPageComponent implements OnInit {
public form: FormGroup; public form: UntypedFormGroup;
public isEditing: boolean; public isEditing: boolean;
public isAdmin: 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 notificationService: NotificationService,
private sanitizer: DomSanitizer) { 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 id="result{{result.id}}" *ngIf="fullyLoaded" class="ombi-card dark-card c" [style.display]="hide ? 'none' : 'block'">
<div class="card-top-info"> <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 }} {{ 'Common.' + RequestType[result.type] | translate }}
</div> </div>
<div class="{{getStatusClass()}} top-right" id="status{{result.id}}"> <div class="{{getStatusClass()}} top-right" id="status{{result.id}}">
<span class="indicator"></span><span class="indicator-text" id="availabilityStatus{{result.id}}">{{getAvailabilityStatus()}}</span> <span class="indicator"></span><span class="indicator-text" id="availabilityStatus{{result.id}}">{{getAvailabilityStatus()}}</span>
</div> </div>
</div> </div>
<img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image" <ombi-image [src]="result.posterPath" [type]="result.type" [routerLink]="generateDetailsLink()" id="cardImage" class="image" alt="{{result.title}}"></ombi-image>
alt="{{result.title}}">
<div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'"> <div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'">
<a class="poster-overlay" [routerLink]="generateDetailsLink()"> <a class="poster-overlay" [routerLink]="generateDetailsLink()">
<div class="summary"> <div class="summary">
@ -48,4 +47,4 @@
</div> </div>
</div> </div>

@ -83,7 +83,7 @@ small {
} }
.image { ::ng-deep .image {
border-radius: 10px; border-radius: 10px;
opacity: 1; opacity: 1;
display: block; 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() { private getExtraMovieInfo() {
if (!this.result.imdbid) { if (!this.result.imdbid) {
this.searchService.getFullMovieDetails(+this.result.id) this.searchService.getFullMovieDetails(+this.result.id)

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

@ -2,7 +2,13 @@
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner"> <div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner> <mat-spinner [color]="'accent'"></mat-spinner>
</div> </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}}"> <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> <discover-card [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card>
</div> </div>

@ -29,6 +29,7 @@ export class DiscoverSearchResultsComponent implements OnInit {
public filter: SearchFilter; public filter: SearchFilter;
private isAdvancedSearch: boolean; private isAdvancedSearch: boolean;
private loadPosition: number = 30;
constructor(private searchService: SearchV2Service, constructor(private searchService: SearchV2Service,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -65,7 +66,7 @@ export class DiscoverSearchResultsComponent implements OnInit {
} }
}); });
if (this.advancedDataService) { if (this.isAdvancedSearch) {
return; return;
} }
this.loadingFlag = true; 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() { private async search() {
this.clear(); this.clear();
this.results = await this.searchService this.results = await this.searchService

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

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

@ -17,3 +17,8 @@ export interface IDiscoverModel {
watchProviders?: number[]; watchProviders?: number[];
companies?: 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; showAdultMovies: boolean;
excludedKeywordIds: number[]; excludedKeywordIds: number[];
excludedMovieGenreIds: number[]; excludedMovieGenreIds: number[];
excludedTvGenreIds: number[] excludedTvGenreIds: number[];
originalLanguages: string[];
} }
export interface IUpdateModel export interface IUpdateModel

@ -14,7 +14,7 @@
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th> <th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element"> <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> </td>
</ng-container> </ng-container>

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

@ -26,15 +26,6 @@ div.centered {
transform: translate(-50%, -50%); 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{ .online{
color:lightgreen; color:lightgreen;

@ -1,48 +1,34 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { APP_BASE_HREF } from "@angular/common";
import { Component, OnDestroy, OnInit, Inject } from "@angular/core"; import { Component, OnInit, Inject } from "@angular/core";
import { IMediaServerStatus } from "../interfaces"; import { IMediaServerStatus } from "../interfaces";
import { ICustomizationSettings, ILandingPageSettings } from "../interfaces"; import { ICustomizationSettings, ILandingPageSettings } from "../interfaces";
import { LandingPageService } from "../services"; import { LandingPageService } from "../services";
import { SettingsService } 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 { CustomizationFacade } from "../state/customization";
import { ThousandShortPipe } from "../pipes/ThousandShortPipe";
@Component({ @Component({
templateUrl: "./landingpage.component.html", templateUrl: "./landingpage.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./landingpage.component.scss"], styleUrls: ["./landingpage.component.scss"],
}) })
export class LandingPageComponent implements OnDestroy, OnInit { export class LandingPageComponent implements OnInit {
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public landingPageSettings: ILandingPageSettings; public landingPageSettings: ILandingPageSettings;
public background: any;
public mediaServerStatus: IMediaServerStatus; public mediaServerStatus: IMediaServerStatus;
public baseUrl: string; public baseUrl: string;
private timer: any;
private href: string; private href: string;
constructor(private settingsService: SettingsService, constructor(private settingsService: SettingsService,
private images: ImageService, private sanitizer: DomSanitizer, private landingPageService: LandingPageService, private landingPageService: LandingPageService,
private customizationFacade: CustomizationFacade, private customizationFacade: CustomizationFacade,
@Inject(APP_BASE_HREF) href :string) { this.href = href } @Inject(APP_BASE_HREF) href :string) { this.href = href }
public ngOnInit() { public ngOnInit() {
this.customizationFacade.settings$().subscribe(x => this.customizationSettings = x); this.customizationFacade.settings$().subscribe(x => this.customizationSettings = x);
this.settingsService.getLandingPage().subscribe(x => this.landingPageSettings = 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; const base = this.href;
if (base.length > 1) { if (base.length > 1) {
@ -53,18 +39,4 @@ export class LandingPageComponent implements OnDestroy, OnInit {
this.mediaServerStatus = x; 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"> <ombi-image-background></ombi-image-background>
<div class="login-gradient-bar">
</div>
</div>
<div class="small-middle-container"> <div class="small-middle-container">
<div *ngIf="form && customizationSettings && authenticationSettings"> <div *ngIf="form && customizationSettings && authenticationSettings">

@ -11,23 +11,6 @@ img.center {
max-width: 100%; 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 { .card-container.card {
max-width: 500px; max-width: 500px;
padding: 45px 45px; padding: 45px 45px;

@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit, Inject } from "@angular/core"; 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 { ActivatedRoute, Router } from "@angular/router";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
@ -10,25 +10,19 @@ import { PlexTvService } from "../services";
import { SettingsService } from "../services"; import { SettingsService } from "../services";
import { StatusService } 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 { StorageService } from "../shared/storage/storage-service";
import { MatSnackBar } from "@angular/material/snack-bar"; import { MatSnackBar } from "@angular/material/snack-bar";
import { CustomizationFacade } from "../state/customization"; import { CustomizationFacade } from "../state/customization";
@Component({ @Component({
templateUrl: "./login.component.html", templateUrl: "./login.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./login.component.scss"], styleUrls: ["./login.component.scss"],
}) })
export class LoginComponent implements OnDestroy, OnInit { export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup; public form: UntypedFormGroup;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public authenticationSettings: IAuthenticationSettings; public authenticationSettings: IAuthenticationSettings;
public plexEnabled: boolean; public plexEnabled: boolean;
public background: any;
public landingFlag: boolean; public landingFlag: boolean;
public baseUrl: string; public baseUrl: string;
public loginWithOmbi: boolean; public loginWithOmbi: boolean;
@ -46,7 +40,6 @@ export class LoginComponent implements OnDestroy, OnInit {
public get appNameTranslate(): object { public get appNameTranslate(): object {
return { appName: this.appName }; return { appName: this.appName };
} }
private timer: any;
private clientId: string; private clientId: string;
private errorBody: string; private errorBody: string;
@ -59,11 +52,9 @@ export class LoginComponent implements OnDestroy, OnInit {
private authService: AuthService, private authService: AuthService,
private router: Router, private router: Router,
private status: StatusService, private status: StatusService,
private fb: FormBuilder, private fb: UntypedFormBuilder,
private settingsService: SettingsService, private settingsService: SettingsService,
private customziationFacade: CustomizationFacade, private customziationFacade: CustomizationFacade,
private images: ImageService,
private sanitizer: DomSanitizer,
private route: ActivatedRoute, private route: ActivatedRoute,
@Inject(APP_BASE_HREF) href: string, @Inject(APP_BASE_HREF) href: string,
private translate: TranslateService, private translate: TranslateService,
@ -111,14 +102,6 @@ export class LoginComponent implements OnDestroy, OnInit {
this.headerAuth(); this.headerAuth();
}); });
this.settingsService.getClientId().subscribe((x) => (this.clientId = x)); 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; const base = this.href;
if (base.length > 1) { if (base.length > 1) {
@ -132,7 +115,7 @@ export class LoginComponent implements OnDestroy, OnInit {
.subscribe((x) => (this.errorValidation = x)); .subscribe((x) => (this.errorValidation = x));
} }
public onSubmit(form: FormGroup) { public onSubmit(form: UntypedFormGroup) {
if (form.invalid) { if (form.invalid) {
this.notify.open(this.errorValidation, "OK", { this.notify.open(this.errorValidation, "OK", {
duration: 300000, duration: 300000,
@ -284,18 +267,6 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
public ngOnDestroy() { public ngOnDestroy() {
clearInterval(this.timer);
clearInterval(this.pinTimer); 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"> <ombi-image-background></ombi-image-background>
<div class="login-gradient-bar">
</div>
</div>
<div class="small-middle-container"> <div class="small-middle-container">
<div *ngIf="form && customizationSettings"> <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 { Component, OnInit, Inject } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { DomSanitizer } from "@angular/platform-browser";
import { fadeInOutAnimation } from "../animations/fadeinout"; import { fadeInOutAnimation } from "../animations/fadeinout";
import { ICustomizationSettings } from "../interfaces"; import { ICustomizationSettings } from "../interfaces";
import { IdentityService, ImageService, NotificationService, SettingsService } from "../services"; import { IdentityService, NotificationService, SettingsService } from "../services";
import { CustomizationFacade } from "../state/customization"; import { CustomizationFacade } from "../state/customization";
@Component({ @Component({
templateUrl: "./resetpassword.component.html", templateUrl: "./resetpassword.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./login.component.scss"], styleUrls: ["./login.component.scss"],
}) })
export class ResetPasswordComponent implements OnInit { export class ResetPasswordComponent implements OnInit {
public form: FormGroup; public form: UntypedFormGroup;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public emailSettingsEnabled: boolean; public emailSettingsEnabled: boolean;
public baseUrl: string; public baseUrl: string;
public background: any;
private href: string; private href: string;
constructor(private identityService: IdentityService, private notify: NotificationService, constructor(private identityService: IdentityService, private notify: NotificationService,
private fb: FormBuilder, private settingsService: SettingsService, @Inject(APP_BASE_HREF) href:string, private fb: UntypedFormBuilder, private settingsService: SettingsService, @Inject(APP_BASE_HREF) href:string,
private images: ImageService, private sanitizer: DomSanitizer, private customizationFacade: CustomizationFacade) { private customizationFacade: CustomizationFacade) {
this.href = href; this.href = href;
this.form = this.fb.group({ this.form = this.fb.group({
email: ["", [Validators.required]], email: ["", [Validators.required]],
@ -32,9 +29,7 @@ export class ResetPasswordComponent implements OnInit {
} }
public ngOnInit() { 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; const base = this.href;
if (base.length > 1) { if (base.length > 1) {
this.baseUrl = base; this.baseUrl = base;
@ -43,7 +38,7 @@ export class ResetPasswordComponent implements OnInit {
this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailSettingsEnabled = x); this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailSettingsEnabled = x);
} }
public onSubmit(form: FormGroup) { public onSubmit(form: UntypedFormGroup) {
if (this.emailSettingsEnabled) { if (this.emailSettingsEnabled) {
if (form.invalid) { if (form.invalid) {

@ -1,11 +1,7 @@
 <ombi-image-background></ombi-image-background>
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background">
<div class="login-gradient-bar">
</div>
</div>
<div class="small-middle-container"> <div class="small-middle-container">
<div *ngIf="form && customizationSettings"> <div *ngIf="form && customizationSettings">
<mat-card class="mat-elevation-z8 top-margin login-card"> <mat-card class="mat-elevation-z8 top-margin login-card">
<H1 *ngIf="!customizationSettings.logo && !customizationSettings.applicationName" class="login_logo">OMBI</H1> <H1 *ngIf="!customizationSettings.logo && !customizationSettings.applicationName" class="login_logo">OMBI</H1>
<H1 *ngIf="customizationSettings.applicationName && !customizationSettings.logo" [ngClass]="{'bigText': customizationSettings.applicationName.length >= 7 && customizationSettings.applicationName.length < 14, 'hugeText': customizationSettings.applicationName.length >= 14 }" class="login_logo custom">{{customizationSettings.applicationName}}</H1> <H1 *ngIf="customizationSettings.applicationName && !customizationSettings.logo" [ngClass]="{'bigText': customizationSettings.applicationName.length >= 7 && customizationSettings.applicationName.length < 14, 'hugeText': customizationSettings.applicationName.length >= 14 }" class="login_logo custom">{{customizationSettings.applicationName}}</H1>
@ -13,7 +9,7 @@
<mat-card-content id="login-box"> <mat-card-content id="login-box">
<div *ngIf="form.value.password !== form.value.confirmPassword" class="alert alert-danger">The passwords do not match</div> <div *ngIf="form.value.password !== form.value.confirmPassword" class="alert alert-danger">The passwords do not match</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger"> <div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('password').hasError('required')">The Password is required</div> <div *ngIf="form.get('password').hasError('required')">The Password is required</div>
<div *ngIf="form.get('email').hasError('required')">The Email is required</div> <div *ngIf="form.get('email').hasError('required')">The Email is required</div>
<div *ngIf="form.get('confirmPassword').hasError('required')">The Confirm Password is required</div> <div *ngIf="form.get('confirmPassword').hasError('required')">The Confirm Password is required</div>
@ -41,16 +37,14 @@
Password is <strong>required</strong></mat-error> Password is <strong>required</strong></mat-error>
</mat-form-field> </mat-form-field>
<button id="reset" mat-raised-button color="accent" [disabled]="form.invalid" type="submit">{{'Reset Password' | translate}}</button> <button id="reset" mat-raised-button color="accent" [disabled]="form.invalid" type="submit">{{'Reset Password' | translate}}</button>
</form> </form>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
</div> </div>

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

@ -10,9 +10,8 @@
<social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id" <social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id"
[hasTrailer]="movie.videos?.results?.length > 0" [imdbId]="movie.imdbId" [hasTrailer]="movie.videos?.results?.length > 0" [imdbId]="movie.imdbId"
[twitter]="movie.externalIds.twitterId" [facebook]="movie.externalIds.facebookId" [twitter]="movie.externalIds?.twitterId" [facebook]="movie.externalIds?.facebookId"
[instagram]="movie.externalIds.instagramId" [available]="movie.available" [plexUrl]="movie.plexUrl" [instagram]="movie.externalIds?.instagramId" [available]="movie.available" [isAdmin]="isAdmin"
[embyUrl]="movie.embyUrl" [jellyfinUrl]="movie.jellyfinUrl" [isAdmin]="isAdmin"
[canShowAdvanced]="showAdvanced && movieRequest" [type]="requestType" [has4KRequest]="movie.has4KRequest" [canShowAdvanced]="showAdvanced && movieRequest" [type]="requestType" [has4KRequest]="movie.has4KRequest"
(openTrailer)="openDialog()" (onAdvancedOptions)="openAdvancedOptions()" (openTrailer)="openDialog()" (onAdvancedOptions)="openAdvancedOptions()"
(onReProcessRequest)="reProcessRequest(false)" (onReProcess4KRequest)="reProcessRequest(true)"> (onReProcessRequest)="reProcessRequest(false)" (onReProcess4KRequest)="reProcessRequest(true)">
@ -24,7 +23,7 @@
<div class="row justify-content-center justify-content-sm-start header-container"> <div class="row justify-content-center justify-content-sm-start header-container">
<div class="details-poster-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> </div>
<!--Next to poster--> <!--Next to poster-->
@ -107,7 +106,7 @@
</span> </span>
</span> </span>
</span> </span>
<span *ngIf="movieRequest?.showSubscribe"> <span *ngIf="movieRequest?.showSubscribe">
<button *ngIf="!movieRequest?.subscribed" (click)="notify()" id="notifyBtn" <button *ngIf="!movieRequest?.subscribed" (click)="notify()" id="notifyBtn"
mat-raised-button class="btn-spacing"> <i class="fas fa-bell"></i> mat-raised-button class="btn-spacing"> <i class="fas fa-bell"></i>
@ -239,44 +238,44 @@
</div> </div>
<mat-accordion class=" mat-elevation-z8 spacing-below "> <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-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{'MediaDetails.RecommendationsTitle' | translate}} {{'MediaDetails.RecommendationsTitle' | translate}}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </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="col-md-2" *ngFor="let r of movie.recommendations?.results">
<div class="sidebar affixable affix-top preview-poster"> <div class="sidebar affixable affix-top preview-poster">
<div class="poster"> <div class="poster">
<a [routerLink]="'/details/movie/'+r.id"> <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}}" src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}"
alt="Poster" style="display: block;"> alt="Poster" style="display: block;"> </ombi-image>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel> <mat-expansion-panel *ngIf="movie.similar?.results?.length > 0">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{'MediaDetails.SimilarTitle' | translate}} {{'MediaDetails.SimilarTitle' | translate}}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </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="col-md-2" *ngFor="let r of movie.similar.results">
<div class="sidebar affixable affix-top preview-poster"> <div class="sidebar affixable affix-top preview-poster">
<div class="poster "> <div class="poster ">
<a [routerLink]="'/details/movie/'+r.id"> <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}}" src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}"
alt="Poster" style="display: block;"> alt="Poster" style="display: block;"></ombi-image>
</a> </a>
</div> </div>
</div> </div>
@ -292,4 +291,4 @@
<div class="bottom-page-gap"> <div class="bottom-page-gap">
</div> </div>
</section> </section>
</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 { 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 { DomSanitizer } from "@angular/platform-browser";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { MatDialog } from "@angular/material/dialog"; 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 { TranslateService } from "@ngx-translate/core";
import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component"; import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component";
import { RequestServiceV2 } from "../../../services/requestV2.service"; import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
import { firstValueFrom, forkJoin } from "rxjs"; import { firstValueFrom, forkJoin } from "rxjs";
import { AdminRequestDialogComponent } from "../../../shared/admin-request-dialog/admin-request-dialog.component"; import { AdminRequestDialogComponent } from "../../../shared/admin-request-dialog/admin-request-dialog.component";
import { FeaturesFacade } from "../../../state/features/features.facade"; import { FeaturesFacade } from "../../../state/features/features.facade";
@ -32,31 +31,45 @@ export class MovieDetailsComponent implements OnInit{
public issuesEnabled: boolean; public issuesEnabled: boolean;
public roleName4k = "Request4KMovie"; public roleName4k = "Request4KMovie";
public is4KEnabled = false; public is4KEnabled = false;
public requestType = RequestType.movie; public requestType = RequestType.movie;
private theMovidDbId: number; private theMovidDbId: number;
private imdbId: string; 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, private sanitizer: DomSanitizer, private imageService: ImageService,
public dialog: MatDialog, private requestService: RequestService, public dialog: MatDialog, private requestService: RequestService,
private requestService2: RequestServiceV2, private radarrService: RadarrService, private requestService2: RequestServiceV2, private radarrService: RadarrService,
public messageService: MessageService, private auth: AuthService, private settingsState: SettingsStateService, public messageService: MessageService, private auth: AuthService, private settingsState: SettingsStateService,
private translate: TranslateService, private featureFacade: FeaturesFacade) { private translate: TranslateService, private featureFacade: FeaturesFacade) {
this.route.params.subscribe(async (params: any) => { this.snapMovieId = this.route.snapshot.params.movieDbId;
if (typeof params.movieDbId === 'string' || params.movieDbId instanceof String) { this.route.params.subscribe(async (params: any) => {
if (params.movieDbId.startsWith("tt")) { if (typeof params.movieDbId === 'string' || params.movieDbId instanceof String) {
this.imdbId = params.movieDbId; if (params.movieDbId.startsWith("tt")) {
} this.imdbId = params.movieDbId;
} // Check if we user navigated to another movie and if so reload the component
this.theMovidDbId = params.movieDbId; 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.is4KEnabled = this.featureFacade.is4kEnabled();
this.issuesEnabled = this.settingsState.getIssue(); this.issuesEnabled = this.settingsState.getIssue();
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
@ -68,6 +81,7 @@ export class MovieDetailsComponent implements OnInit{
if (this.imdbId) { if (this.imdbId) {
this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => { this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => {
this.movie = x; this.movie = x;
this.checkPoster();
if (this.movie.requestId > 0) { if (this.movie.requestId > 0) {
// Load up this request // Load up this request
this.hasRequest = true; this.hasRequest = true;
@ -78,6 +92,7 @@ export class MovieDetailsComponent implements OnInit{
} else { } else {
this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(async x => { this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(async x => {
this.movie = x; this.movie = x;
this.checkPoster();
if (this.movie.requestId > 0) { if (this.movie.requestId > 0) {
// Load up this request // Load up this request
this.hasRequest = true; 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() { private loadAdvancedInfo() {
const profile = this.radarrService.getQualityProfilesFromSettings(); const profile = this.radarrService.getQualityProfilesFromSettings();
const folders = this.radarrService.getRootFoldersFromSettings(); const folders = this.radarrService.getRootFoldersFromSettings();

@ -30,28 +30,35 @@
<span id="status"> {{ this.movie.status | translateStatus }}</span> <span id="status"> {{ this.movie.status | translateStatus }}</span>
</div> </div>
<div> <div>
<span class="label">{{'MediaDetails.Availability' | translate }}</span> <span class="label">{{'MediaDetails.Availability' | translate }}</span>
<span *ngIf="movie.available"> {{'Common.Available' | translate}}</span> <span *ngIf="movie.available || movie.available4K"> {{'Common.Available' | translate}}</span>
<span *ngIf="!movie.available"> {{'Common.NotAvailable' | translate}}</span> <span *ngIf="!movie.available && !movie.available4K"> {{'Common.NotAvailable' | translate}}</span>
</div> </div>
<div *ngIf="!movie.available"> <div *ngIf="(!movie.available && movie.requested) || (!movie.available4K && movie.has4KRequest)">
<span class="label">{{'MediaDetails.RequestStatus' | translate }}</span> <span class="label">{{'MediaDetails.RequestStatus' | translate }}</span>
<div *ngIf="movie.denied">{{'Common.Denied' | translate}}</div> <div *ngIf="!movie.available && movie.requested">
<div *ngElseIf="movie.approved && !movie.available">{{'Common.ProcessingRequest' | translate}}</div> <div *ngIf="movie.denied">{{'Common.RequestDenied' | translate}}</div>
<div *ngElseIf="movie.requested && !movie.approved && !movie.available">{{'Common.PendingApproval' | translate}} <div *ngIf="movie.approved && !movie.available">{{'Common.ProcessingRequest' | translate}}</div>
</div> <div *ngIf="movie.requested && !movie.approved">{{'Common.PendingApproval' | translate}}</div>
<div *ngElseIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}} <!--<div *ngIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}}</div>-->
</div> </div>
<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> </div>
<div *ngIf="request"> <div *ngIf="request">
<span class="label">{{'MediaDetails.RequestedBy' | translate }}</span> <span class="label">{{'MediaDetails.RequestedBy' | translate }}</span>
<span id="requestedByInfo"> {{request.requestedUser.userAlias}}</span> <span id="requestedByInfo"> {{request.requestedUser.userAlias}}</span>
</div> </div>
<div *ngIf="request"> <div *ngIf="request">
<span class="label">{{'MediaDetails.RequestDate' | translate }}</span> <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>
<div *ngIf="request && request.source !== RequestSource.Ombi"> <div *ngIf="request && request.source !== RequestSource.Ombi">
@ -59,9 +66,14 @@
{{RequestSource[request.source]}} {{RequestSource[request.source]}}
</div> </div>
<div *ngIf="request && request.denied"> <div *ngIf="request">
<span class="label">{{'MediaDetails.DeniedReason' | translate }}</span> <span class="label">{{'MediaDetails.DeniedReason' | translate }}</span>
<div *ngIf="request.denied">
<span id="deniedReasonInfo">{{request.deniedReason}}</span> <span id="deniedReasonInfo">{{request.deniedReason}}</span>
</div>
<div *ngIf="request.denied4K">
<span id="deniedReasonInfo4K">{{request.deniedReason4K}}</span>
</div>
</div> </div>
@ -72,7 +84,7 @@
<div *ngIf="movie.available4K"> <div *ngIf="movie.available4K">
<span class="label">{{'MediaDetails.Quality' | translate }}&nbsp;</span> <span class="label">{{'MediaDetails.Quality' | translate }}&nbsp;</span>
<span >{{"4K" | quality}}</span> <span>{{"4K" | quality}}</span>
</div> </div>
<div *ngIf="advancedOptions && request && request.rootPathOverrideTitle"> <div *ngIf="advancedOptions && request && request.rootPathOverrideTitle">

@ -6,10 +6,10 @@
<div class="row justify-content-md-center mat-card mat-card-flat carousel-item"> <div class="row justify-content-md-center mat-card mat-card-flat carousel-item">
<div class="bottom-space"> <div class="bottom-space">
<a *ngIf="item.image" [routerLink]="'/discover/actor/' + item.id"> <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>
<a *ngIf="item.profile_path" [routerLink]="'/discover/actor/' + item.id"> <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> </a>
<!-- TODO get profile image default --> <!-- 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) { @media (min-width: 979px) {
.cast-profile-img { :host ::ng-deep .cast-profile-img {
border-radius: 100%; border-radius: 100%;
width: 200px; width: 200px;
max-height: 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) { @media (max-width: 978px) {
.cast-profile-img { :host ::ng-deep .cast-profile-img {
border-radius: 100%; border-radius: 100%;
width: 100px; width: 100px;
max-height: 100px; max-height: 100px;

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

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

@ -11,7 +11,7 @@
<i matTooltip="The TV DB" class="fas fa-tv fa-2x grow-social"></i> <i matTooltip="The TV DB" class="fas fa-tv fa-2x grow-social"></i>
</a> </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> <i matTooltip="Trailer" class="fab fa-youtube fa-2x grow-social"></i>
</a> </a>
<a *ngIf="imdbId" class="media-icons imdb" [href]="doNotAppend ? imdbid : 'https://imdb.com/title/' + imdbId" target="_blank"> <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; padding: 0;
} }
button.admin-cog{ button.admin-cog{
margin-left:40px; margin-left:40px;
color:$ombi-active; color:$ombi-active;

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

Loading…
Cancel
Save