* 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]

* fix: Log Microsoft warnings to log file (#4723)

[skip ci]

* feat:  Recently Requested on Discover Page (#4387)

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

* fix: Localize recently requested on discover page (#4729)

[skip ci]

* 🌐 Translations Update (#4731)

* 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]

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

* Fix: Ombi.Api.Lidarr: Remove unused fields from ArtistAdd (#4727)

When an artist is not found in Lidarr as part of requesting an album,
MusicSender will make a POST request against the /api/v1/artist endpoint
to add such artist.

Not all fields defined in ArtistAdd are initialized, and those
uninitialized will be `null` values in the JSON body of the request, as
shown in this intercepted request:

```
{
    "addOptions": {
        "AlbumsToMonitor": [
            "e5c48b66-44ef-3685-ad53-45dbcd7294c0"
        ],
        "monitor": 6,
        "monitored": true,
        "searchForMissingAlbums": false
    },
    "added": "2022-08-10T06:49:32.4374278+00:00",
    "albumFolder": true,
    "artistName": "Manolo García",
    "cleanName": "manologarcía",
    "disambiguation": null,
    "discogsId": 0,
    "ended": false,
    "foreignArtistId": "1c8309da-9789-40bf-b9c2-e20064263820",
    "images": [],
    "links": [],
    "metadataProfileId": 1,
    "monitored": true,
    "overview": null,
    "qualityProfileId": 3,
    "ratings": null,
    "remotePoster": null,
    "rootFolderPath": "/media/music/",
    "sortName": null,
    "statistics": null,
    "status": null,
    "tadbId": 0,
    "tags": null
}
```

This request will fail and Lidarr will return a 400 BadRequest error
with the following message:

```
2022-08-10 01:45:52.458 +00:00 [Error] StatusCode: BadRequest, Reason: Bad Request, RequestUri: http://lidarr:8686/api/v1/artist
2022-08-10 01:45:52.459 +00:00 [Debug] {
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-52e01b322a05d7c6633eca2488ef2a5c-06345b3bb8c4bb6c-00",
  "errors": {
    "$.status": [
      "The JSON value could not be converted to NzbDrone.Core.Music.ArtistStatusType. Path: $.status | LineNumber: 0 | BytePositionInLine: 14."
    ]
  }
}
```

Removing all the `null` fields from the JSON body fixes the problem and
correctly adds the artist to Lidarr.

* chore: 👥 Updated Contributors [skip ci]

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

* fix: Fix conflicting property name for Swagger (#4733)

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

* feat: add crew on movie page (#4722)

* add crew on movie page

* order by director, add default image and fix click

Co-authored-by: tidusjar <tidusjar@gmail.com>

* 🌐 Translations Update (#4736)

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

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

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

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

* feat: Watchlist history errors(#4741)

[skip ci]

* fix: fixed stats controller (#4742)

* chore: 👥 Updated Contributors [skip ci]

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

* fix(webhook): Remove added trailing slash from webhook URL #4710

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

* feat(notifications): Add more curly variables for partially available notification

* feat: Add more curly variables for partially available notification

* test: Fix newly added test

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

* feat: Recently requested improvements (#4755)

* feat(discover):  Admins can now approve the Recently Requested list

* feat(discover):  Images for the recently requested area are now loading faster and just better all around

* test:  Added automation for the new feature

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

* fix(plex): stop the plex sync from deleting episodes when we can't find the plex key

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

* refactor: Encapsulate common TV availability checker logic (#4753)

[skip ci]

* fix(sonarr): 🐛 Cleaned up and removed Sonarr v3 option, sonarr v3 is now the default. This allows us to get ready for the upcoming Sonarr v4 (#4764)

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

* 🌐 Translations Update (#4739)

* 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]

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

* fix(availability): 🐛 Fixed a issue with the availability checker after the previous update. Added full test coverage around that area

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

* test: 🧪 added full test coverage to the plex availability checker, also fixed a small few bugs in there at the same time

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

* fix(importer): 🐛 Allow you to only import Plex Admins without the Plex Users

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

* fix(notifications): Fixed the error when sending multiple test notifications. Added more logging when Discord complains the message is invalid

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

* fix: Fixes default image for recently requested items. (#4767)

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

* refactor: Upgrades nuget packages. Removes deprecated packages. Fixes build warnings. (#4769)

* Upgrades nuget packages. Removes deprecated packages. Fixes build warnings.

* Fixes the last few build warnings.

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

* refactor: Rework the Plex Settings Page (#4772)

[skip ci]

* feat(plex):  Added the ability to configure the watchlist to request the whole TV show rather than latest season (#4774)

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

* 🌐 Translations Update (#4771)

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

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

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

* 🌐 Translations Update (#4775)

* 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: Reworked the version check (#4719) (#4781)

[skip ci]

* fix(plex): 🐛 Fixed not being able to enable watchlist requests in the Plex settings

* chore: 👥 Updated Contributors [skip ci]

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

* feat: Provide a flag for missing users on Plex Server (#4688) (#4778)

[skip ci]

* fix: Unable to Delete Jellyfin Server (#4705) (#4780)

[skip ci]

* fix: Partially Available prevents further TV requests (#4768) (#4779)

* chore: 👥 Updated Contributors [skip ci]

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

* fix: Consistently reset loading flag when requesting movies on discover page. (#4777)

[skip ci]

* fix(sonarr): 🐛 Fixed an issue where the language list didn't correctly load for power users in the advanced options #4782

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

* fix(plex): Fixed an issue where sometimes the availability checker would throw an exception when checking episodes

* chore: fixed tests

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

* fix: Only log error messages from Microsoft (#4787)

[skip ci]

* 🌐 Translations Update (#4784)

* 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(notifications): Fixed the Partially TV notifications going to the admin #4797 (#4799)

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

* feat(sonarr):  Add the username to a Sonarr tag when sent to Sonarr (#4802)

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

* feat(sonarr): Added the ability to add default tags when sending to Sonarr (#4803)

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

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

* feat(plex): Rework the Plex Settings page (#4805)

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

* fix(plex): 🐛 Fixed the issue where you couldn't add a new server on a fresh setup after the settings page rework

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

* fix(sonarr): 🐛 Sonarr V4 should work now (#4810)

* fix(sonarr): 🐛 Sonarr V4 should work now

Auto detect the sonarr version and adjust the UI depending on V3 or V4 (Lang profiles)

* fix: Fixed the load error

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

* fix(sonarr): V4 actually works this time around

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

* feat: Angular 15 and Dependency upgrades (#4818)

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

* fix(plex): Added the watchlist request whole show back into the settings

* chore: undid

* fixed (#4833)

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

* chore: add logo [skip ci]

* feat: Radarr tags (#4815)

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

* fix(plex-watchlist): Lookup the ID from different sources when Plex doesn't contain the metadata (#4843)

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

* feat: Add the option for header authentication to create users (#4841)

* feat: allow SSO to create new users automatically

* feat: apply default user settings to SSO users

* feat: add warnings to header auth toggles

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

* fix(plex-watchlist): Index out of bounds error

* chore: 👥 Updated Contributors [skip ci]

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

* fix(database): Just some tweaks, shouldn't notice any difference, maybe a less error in the log

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

* fix(#4847): Invalid Discord request fixed, also fixed an issue where App Only users would not show as logged in on the user management page (#4848)

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

* bug(#4854): 🐛 Fixed the Recently Requested showing requests when it should be hidden

* fix(discover): 🐛 Fixed the default poster not taking into account the base url in some scenarios #4845

* fix(Hide music from navbar and request list when not enabled): 🐛

* chore: 👥 Updated Contributors [skip ci]

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

* fix(radarr-settings): 🐛 Fixed a typo

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

* fix: Fixed the issue where the login page is still present after logging in with oauth

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

* fix(wizard): 🐛 Stop access to the wizard when you have already setup ombi (#4866)

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

* fix(plex-oauth): 🐛 Fixed an issue where using OAuth you could log in as a Ombi Local user #4835

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

* chore: 👥 Updated Contributors [skip ci]

* fixed bad merge

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

* Update .gitignore

* Fixed automation

---------

Signed-off-by: Jamie <tidusjar@gmail.com> [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>
Co-authored-by: Miguel A Vico Moya <mvicomoya@gmail.com>
Co-authored-by: Hadrien <26697460+ketsapiwiq@users.noreply.github.com>
Co-authored-by: Victor Usoltsev <bernarden@users.noreply.github.com>
Co-authored-by: Wesley King <kingwe92@gmail.com>
Co-authored-by: Lea <me@janderedev.xyz>
pull/4931/head^2
Jamie 1 year ago committed by GitHub
parent 6cb0bf58c6
commit 3da9a40c40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '18'
- name: NodeModules Cache - name: NodeModules Cache
uses: actions/cache@v2 uses: actions/cache@v2

@ -22,7 +22,7 @@ jobs:
dotnet-version: 6.0.x dotnet-version: 6.0.x
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '18'
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
@ -43,6 +43,9 @@ jobs:
- name: Run Docker Image - name: Run Docker Image
run: nohup docker run --rm -p 5000:5000 ombi & run: nohup docker run --rm -p 5000:5000 ombi &
# - name: Run Wiremock Plex
# run: nohup docker run -it --rm -p 32400:8080 --name wiremock wiremock/wiremock:2.35.0
- name: Sleep for server to start - name: Sleep for server to start
run: sleep 20 run: sleep 20

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '18'
- name: NodeModules Cache - name: NodeModules Cache
uses: actions/cache@v2 uses: actions/cache@v2

3
.gitignore vendored

@ -7,7 +7,7 @@
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files
*.userprefs *.userprefs
# Build results # Build results
@ -252,3 +252,4 @@ _Pvt_Extensions
/src/Ombi/healthchecksdb /src/Ombi/healthchecksdb
/src/Ombi/ClientApp/package-lock.json /src/Ombi/ClientApp/package-lock.json
/src/Ombi.Core/Properties/launchSettings.json /src/Ombi.Core/Properties/launchSettings.json
.yarn

@ -1,355 +1,369 @@
## [4.22.5](https://github.com/Ombi-app/Ombi/compare/v4.16.12...v4.22.5) (2022-08-05) ## [4.35.9](https://github.com/Ombi-app/Ombi/compare/v4.35.8...v4.35.9) (2023-02-24)
## [4.16.12](https://github.com/Ombi-app/Ombi/compare/v4.16.11...v4.16.12) (2022-04-19) ## [4.22.5](https://github.com/Ombi-app/Ombi/compare/v4.22.4...v4.22.5) (2022-08-05)
## [4.16.11](https://github.com/Ombi-app/Ombi/compare/v4.16.10...v4.16.11) (2022-04-14) ## [4.35.8](https://github.com/Ombi-app/Ombi/compare/v4.35.7...v4.35.8) (2023-02-17)
### 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)) * **plex-oauth:** 🐛 Fixed an issue where using OAuth you could log in as a Ombi Local user [#4835](https://github.com/Ombi-app/Ombi/issues/4835) ([4098da3](https://github.com/Ombi-app/Ombi/commit/4098da305aaea9dae9a552644268a4fed7204cfe))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4fc2c1f](https://github.com/Ombi-app/Ombi/commit/4fc2c1f24534085a783a3d5791f5533b68272153))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76ab733](https://github.com/Ombi-app/Ombi/commit/76ab733b91791e4d93d184f3c7d0779c6a388695)) ## [4.35.7](https://github.com/Ombi-app/Ombi/compare/v4.35.6...v4.35.7) (2023-02-10)
* **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.16.10](https://github.com/Ombi-app/Ombi/compare/v4.16.9...v4.16.10) (2022-04-13)
## [4.16.9](https://github.com/Ombi-app/Ombi/compare/v4.16.8...v4.16.9) (2022-04-13)
### 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)) * **wizard:** :bug: Stop access to the wizard when you have already setup ombi ([#4866](https://github.com/Ombi-app/Ombi/issues/4866)) ([353de98](https://github.com/Ombi-app/Ombi/commit/353de981a462e1753288d225ec4644a44a62d2bc))
## [4.16.8](https://github.com/Ombi-app/Ombi/compare/v4.16.7...v4.16.8) (2022-04-13) ## [4.35.6](https://github.com/Ombi-app/Ombi/compare/v4.35.5...v4.35.6) (2023-01-31)
### 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)) * Fixed the issue where the login page is still present after logging in with oauth ([aca4ee3](https://github.com/Ombi-app/Ombi/commit/aca4ee37915a28200e5233be3dc711ccc4a5aee9))
## [4.16.7](https://github.com/Ombi-app/Ombi/compare/v4.16.6...v4.16.7) (2022-04-12) ## [4.35.5](https://github.com/Ombi-app/Ombi/compare/v4.35.4...v4.35.5) (2023-01-24)
### Bug Fixes
## [4.16.6](https://github.com/Ombi-app/Ombi/compare/v4.16.5...v4.16.6) (2022-04-11) * **radarr-settings:** 🐛 Fixed a typo ([4a50a00](https://github.com/Ombi-app/Ombi/commit/4a50a00d4729d99f4359874b9af4dbc58a0c220b))
## [4.16.5](https://github.com/Ombi-app/Ombi/compare/v4.16.4...v4.16.5) (2022-04-08) ## [4.35.4](https://github.com/Ombi-app/Ombi/compare/v4.35.3...v4.35.4) (2023-01-22)
### Bug Fixes ### Bug Fixes
* **watchlist:** actually fixed it this time... ([d962a32](https://github.com/Ombi-app/Ombi/commit/d962a3211eca29520662ddce962676e3aea17ec5)) * **discover:** :bug: Fixed the default poster not taking into account the base url in some scenarios [#4845](https://github.com/Ombi-app/Ombi/issues/4845) ([8eda250](https://github.com/Ombi-app/Ombi/commit/8eda250367953183daec03ccb5cdf9fe94275b27))
* **Hide music from navbar and request list when not enabled:** :bug: ([5123a76](https://github.com/Ombi-app/Ombi/commit/5123a76954e9f81d58c05e31afc7a29aec19cb7a))
## [4.16.4](https://github.com/Ombi-app/Ombi/compare/v4.16.3...v4.16.4) (2022-04-08) ## [4.35.3](https://github.com/Ombi-app/Ombi/compare/v4.35.2...v4.35.3) (2023-01-13)
### Bug Fixes
## [4.16.3](https://github.com/Ombi-app/Ombi/compare/v4.16.2...v4.16.3) (2022-04-08) * **#4847:** Invalid Discord request fixed, also fixed an issue where App Only users would not show as logged in on the user management page ([#4848](https://github.com/Ombi-app/Ombi/issues/4848)) ([f229d88](https://github.com/Ombi-app/Ombi/commit/f229d88bd744bc5253b5d3db69ae5ef22d014230))
## [4.35.2](https://github.com/Ombi-app/Ombi/compare/v4.35.1...v4.35.2) (2023-01-08)
### Bug Fixes ### 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)) * **database:** Just some tweaks, shouldn't notice any difference, maybe a less error in the log ([67fb992](https://github.com/Ombi-app/Ombi/commit/67fb9921c0c025025286eb6c0a9d09fd01b18465))
## [4.16.2](https://github.com/Ombi-app/Ombi/compare/v4.16.1...v4.16.2) (2022-04-07) ## [4.35.1](https://github.com/Ombi-app/Ombi/compare/v4.35.0...v4.35.1) (2023-01-06)
### Bug Fixes ### Bug Fixes
* **wizard:** Fixed an issue when using Plex OAuth it could fail setting up ([b743cf4](https://github.com/Ombi-app/Ombi/commit/b743cf4fafa7341ad1b163276f006d7ab0e9dcff)) * **plex-watchlist:** Index out of bounds error ([8cd556e](https://github.com/Ombi-app/Ombi/commit/8cd556e268931596b9c1d1ae0ce533bfaaf330f4))
## [4.16.1](https://github.com/Ombi-app/Ombi/compare/v4.16.0...v4.16.1) (2022-04-07) # [4.35.0](https://github.com/Ombi-app/Ombi/compare/v4.34.1...v4.35.0) (2023-01-04)
### Features
# [4.16.0](https://github.com/Ombi-app/Ombi/compare/v4.15.6...v4.16.0) (2022-04-07) * Add the option for header authentication to create users ([#4841](https://github.com/Ombi-app/Ombi/issues/4841)) ([e6c9ce5](https://github.com/Ombi-app/Ombi/commit/e6c9ce5ad0056608ecda8273fb8124ed292e2942))
## [4.15.6](https://github.com/Ombi-app/Ombi/compare/v4.15.5...v4.15.6) (2022-04-07) ## [4.34.1](https://github.com/Ombi-app/Ombi/compare/v4.34.0...v4.34.1) (2023-01-04)
### 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-watchlist:** Lookup the ID from different sources when Plex doesn't contain the metadata ([#4843](https://github.com/Ombi-app/Ombi/issues/4843)) ([a2cc23b](https://github.com/Ombi-app/Ombi/commit/a2cc23b351c4a568c44e6c855f94db9f71ad084a))
## [4.15.5](https://github.com/Ombi-app/Ombi/compare/v4.15.4...v4.15.5) (2022-04-06) # [4.34.0](https://github.com/Ombi-app/Ombi/compare/v4.33.1...v4.34.0) (2023-01-04)
### Features
## [4.15.4](https://github.com/Ombi-app/Ombi/compare/v4.15.3...v4.15.4) (2022-03-29) * Radarr tags ([#4815](https://github.com/Ombi-app/Ombi/issues/4815)) ([6fa5064](https://github.com/Ombi-app/Ombi/commit/6fa506491fe867cdeef9df79991ae49319d71c3d))
## [4.15.3](https://github.com/Ombi-app/Ombi/compare/v4.15.2...v4.15.3) (2022-03-24) ## [4.33.1](https://github.com/Ombi-app/Ombi/compare/v4.33.0...v4.33.1) (2022-12-22)
### Bug Fixes
## [4.15.2](https://github.com/Ombi-app/Ombi/compare/v4.15.1...v4.15.2) (2022-03-23) * **plex:** Added the watchlist request whole show back into the settings ([10701c4](https://github.com/Ombi-app/Ombi/commit/10701c4a0b6190eebb75c5d8b18224f3d0bc8502))
### 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.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01)
### Features
* Angular 15 and Dependency upgrades ([#4818](https://github.com/Ombi-app/Ombi/issues/4818)) ([4816acf](https://github.com/Ombi-app/Ombi/commit/4816acf6f94443d23ebef6091d4cfcbca580f9ca))
## [4.15.1](https://github.com/Ombi-app/Ombi/compare/v4.15.0...v4.15.1) (2022-03-18)
## [4.32.3](https://github.com/Ombi-app/Ombi/compare/v4.32.2...v4.32.3) (2022-11-24)
### Bug Fixes ### 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)) * **sonarr:** V4 actually works this time around ([f62e70f](https://github.com/Ombi-app/Ombi/commit/f62e70fc493c7971da5e4508ce10522f5df0bbf7))
# [4.15.0](https://github.com/Ombi-app/Ombi/compare/v4.14.4...v4.15.0) (2022-03-17) ## [4.32.2](https://github.com/Ombi-app/Ombi/compare/v4.32.1...v4.32.2) (2022-11-23)
### Bug Fixes ### Bug Fixes
* **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) * **sonarr:** :bug: Sonarr V4 should work now ([#4810](https://github.com/Ombi-app/Ombi/issues/4810)) ([37655af](https://github.com/Ombi-app/Ombi/commit/37655aff9d3d133b42f5664bc9445d6571e966d6))
## [4.14.4](https://github.com/Ombi-app/Ombi/compare/v4.14.3...v4.14.4) (2022-03-10) ## [4.32.1](https://github.com/Ombi-app/Ombi/compare/v4.32.0...v4.32.1) (2022-11-21)
### 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) * **plex:** :bug: Fixed the issue where you couldn't add a new server on a fresh setup after the settings page rework ([187b18d](https://github.com/Ombi-app/Ombi/commit/187b18d5c01f6a13831e4a410b5d7c349e27d847))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([356c742](https://github.com/Ombi-app/Ombi/commit/356c7424e0ce8c1c5063b04bc6ed9b809f214d65))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6fcaecf](https://github.com/Ombi-app/Ombi/commit/6fcaecf80b766f2d43ac7082d74364238e1638b7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([132f4d4](https://github.com/Ombi-app/Ombi/commit/132f4d4e609b7fb7e37f38ee2f395926e2911abe))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f292006](https://github.com/Ombi-app/Ombi/commit/f292006a08894a8d0ba899c8c6e9fe863e558dda))
## [4.14.3](https://github.com/Ombi-app/Ombi/compare/v4.14.2...v4.14.3) (2022-03-06) # [4.32.0](https://github.com/Ombi-app/Ombi/compare/v4.31.0...v4.32.0) (2022-11-18)
### Bug Fixes ### Bug Fixes
* **availability:** :bug: Fixed an issue where with 4k content, we could repeat notifications ([f9ebc1c](https://github.com/Ombi-app/Ombi/commit/f9ebc1cc2e13c7cd335121cd86295b10eda529ba)) * **translations:** 🌐 New translations from Crowdin [skip ci] ([#4801](https://github.com/Ombi-app/Ombi/issues/4801)) ([4692003](https://github.com/Ombi-app/Ombi/commit/46920032baed04675b2ffbe1700afdc0740a4ac4))
### Features
* **plex:** Rework the Plex Settings page ([#4805](https://github.com/Ombi-app/Ombi/issues/4805)) ([1b8c47f](https://github.com/Ombi-app/Ombi/commit/1b8c47f3163f618851d4904732cb07015e1e93ff))
# [4.31.0](https://github.com/Ombi-app/Ombi/compare/v4.30.0...v4.31.0) (2022-11-18)
### Features
* **sonarr:** Added the ability to add default tags when sending to Sonarr ([#4803](https://github.com/Ombi-app/Ombi/issues/4803)) ([ecfbb8e](https://github.com/Ombi-app/Ombi/commit/ecfbb8eda91e1a90239dcf8be847afcc2394a78e))
## [4.14.2](https://github.com/Ombi-app/Ombi/compare/v4.14.1...v4.14.2) (2022-03-05) # [4.30.0](https://github.com/Ombi-app/Ombi/compare/v4.29.3...v4.30.0) (2022-11-17)
### Features
* **sonarr:** :sparkles: Add the username to a Sonarr tag when sent to Sonarr ([#4802](https://github.com/Ombi-app/Ombi/issues/4802)) ([1d5fabd](https://github.com/Ombi-app/Ombi/commit/1d5fabd317e3ce8f6dd31f06d15dc81277f39dbd))
## [4.29.3](https://github.com/Ombi-app/Ombi/compare/v4.29.2...v4.29.3) (2022-11-14)
### Bug Fixes ### 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) * **notifications:** Fixed the Partially TV notifications going to the admin [#4797](https://github.com/Ombi-app/Ombi/issues/4797) ([#4799](https://github.com/Ombi-app/Ombi/issues/4799)) ([bcb3e7f](https://github.com/Ombi-app/Ombi/commit/bcb3e7f00380a4c4278f59dc55febf43e6d05d47))
* Only log error messages from Microsoft ([#4787](https://github.com/Ombi-app/Ombi/issues/4787)) ([c614e0c](https://github.com/Ombi-app/Ombi/commit/c614e0ca5fe5023cbe7ced326145273cd75be85d))
## [4.14.1](https://github.com/Ombi-app/Ombi/compare/v4.14.0...v4.14.1) (2022-03-03) ## [4.29.2](https://github.com/Ombi-app/Ombi/compare/v4.29.1...v4.29.2) (2022-10-24)
### Bug Fixes
# [4.14.0](https://github.com/Ombi-app/Ombi/compare/v4.13.2...v4.14.0) (2022-03-02) * **plex:** Fixed an issue where sometimes the availability checker would throw an exception when checking episodes ([17ba202](https://github.com/Ombi-app/Ombi/commit/17ba2020ee0950c2c0e0e03fdb7835b579da75a9))
## [4.13.2](https://github.com/Ombi-app/Ombi/compare/v4.13.1...v4.13.2) (2022-03-01) ## [4.29.1](https://github.com/Ombi-app/Ombi/compare/v4.29.0...v4.29.1) (2022-10-22)
### Bug Fixes ### 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)) * Consistently reset loading flag when requesting movies on discover page. ([#4777](https://github.com/Ombi-app/Ombi/issues/4777)) ([a40ab5c](https://github.com/Ombi-app/Ombi/commit/a40ab5cddf769d4147696eca50c1610b466ab99b))
* **sonarr:** :bug: Fixed an issue where the language list didn't correctly load for power users in the advanced options [#4782](https://github.com/Ombi-app/Ombi/issues/4782) ([2173670](https://github.com/Ombi-app/Ombi/commit/217367047d1568070dd507e54ad3fd2c68f05b88))
## [4.13.1](https://github.com/Ombi-app/Ombi/compare/v4.13.0...v4.13.1) (2022-03-01) # [4.29.0](https://github.com/Ombi-app/Ombi/compare/v4.28.1...v4.29.0) (2022-10-19)
### Bug Fixes ### 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)) * Partially Available prevents further TV requests ([#4768](https://github.com/Ombi-app/Ombi/issues/4768)) ([#4779](https://github.com/Ombi-app/Ombi/issues/4779)) ([031e2b9](https://github.com/Ombi-app/Ombi/commit/031e2b9283b239827cabaca4e35f69f2f93a4d7b))
* **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)) * Unable to Delete Jellyfin Server ([#4705](https://github.com/Ombi-app/Ombi/issues/4705)) ([#4780](https://github.com/Ombi-app/Ombi/issues/4780)) ([76a0d0d](https://github.com/Ombi-app/Ombi/commit/76a0d0d26893bd480fea4735f77522ac6261a425))
* **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))
### Features
# [4.13.0](https://github.com/Ombi-app/Ombi/compare/v4.12.7...v4.13.0) (2022-02-25) * Provide a flag for missing users on Plex Server ([#4688](https://github.com/Ombi-app/Ombi/issues/4688)) ([#4778](https://github.com/Ombi-app/Ombi/issues/4778)) ([b4a14c2](https://github.com/Ombi-app/Ombi/commit/b4a14c2d28218409390e517b226130e3e84efee1))
## [4.28.1](https://github.com/Ombi-app/Ombi/compare/v4.28.0...v4.28.1) (2022-10-19)
### 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)) * **plex:** :bug: Fixed not being able to enable watchlist requests in the Plex settings ([3e5158e](https://github.com/Ombi-app/Ombi/commit/3e5158ef9cda58ea2dd3be143f07aa5433691d79))
* **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)) * Reworked the version check ([#4719](https://github.com/Ombi-app/Ombi/issues/4719)) ([#4781](https://github.com/Ombi-app/Ombi/issues/4781)) ([55855c5](https://github.com/Ombi-app/Ombi/commit/55855c5adda3cd1c51b7fbd0c19b469fc813f98e))
* **localisation:** Localize request types in notifications ([#4516](https://github.com/Ombi-app/Ombi/issues/4516)) ([e09435d](https://github.com/Ombi-app/Ombi/commit/e09435da455b12fc429f129372de31e0654da797))
* **notifications:** Remove generic admin email in favour of admins' email ([#4519](https://github.com/Ombi-app/Ombi/issues/4519)) ([b90fc5f](https://github.com/Ombi-app/Ombi/commit/b90fc5fea771a83e6cf576c71a307066efd59ea4))
* **tv:** Display TV show as requested if all episodes are requested ([#4518](https://github.com/Ombi-app/Ombi/issues/4518)) ([2ed8c48](https://github.com/Ombi-app/Ombi/commit/2ed8c48d128a69f0d144c5d332286dbf3b0bdf28))
# [4.28.0](https://github.com/Ombi-app/Ombi/compare/v4.27.8...v4.28.0) (2022-10-07)
### Features ### 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)) * **plex:** ✨ Added the ability to configure the watchlist to request the whole TV show rather than latest season ([#4774](https://github.com/Ombi-app/Ombi/issues/4774)) ([fa65712](https://github.com/Ombi-app/Ombi/commit/fa65712bd570fe8d5d21b8ca0abe182b84960017))
* **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.27.8](https://github.com/Ombi-app/Ombi/compare/v4.27.7...v4.27.8) (2022-10-07)
## [4.12.7](https://github.com/Ombi-app/Ombi/compare/v4.12.6...v4.12.7) (2022-02-23) ## [4.27.7](https://github.com/Ombi-app/Ombi/compare/v4.27.6...v4.27.7) (2022-10-07)
### Bug Fixes
* Fixes default image for recently requested items. ([#4767](https://github.com/Ombi-app/Ombi/issues/4767)) ([2e6f35f](https://github.com/Ombi-app/Ombi/commit/2e6f35f89abb3dd3685ec8289f8620c7ef7072cd))
## [4.12.6](https://github.com/Ombi-app/Ombi/compare/v4.12.5...v4.12.6) (2022-02-22)
## [4.27.6](https://github.com/Ombi-app/Ombi/compare/v4.27.5...v4.27.6) (2022-10-01)
### 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)) * **notifications:** Fixed the error when sending multiple test notifications. Added more logging when Discord complains the message is invalid ([fc14780](https://github.com/Ombi-app/Ombi/commit/fc14780bd354483119ddcbb55a8c382e1890a783))
* **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.27.5](https://github.com/Ombi-app/Ombi/compare/v4.27.4...v4.27.5) (2022-09-30)
### 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)) * **importer:** 🐛 Allow you to only import Plex Admins without the Plex Users ([8c9ad9b](https://github.com/Ombi-app/Ombi/commit/8c9ad9b414fdc6c88bdb911d6057ae5d38783b98))
* **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.27.4](https://github.com/Ombi-app/Ombi/compare/v4.27.3...v4.27.4) (2022-09-30)
## [4.12.3](https://github.com/Ombi-app/Ombi/compare/v4.12.2...v4.12.3) (2022-02-16) ## [4.27.3](https://github.com/Ombi-app/Ombi/compare/v4.27.2...v4.27.3) (2022-09-30)
### Bug Fixes
## [4.12.2](https://github.com/Ombi-app/Ombi/compare/v4.12.1...v4.12.2) (2022-02-16) * **availability:** 🐛 Fixed a issue with the availability checker after the previous update. Added full test coverage around that area ([28e2480](https://github.com/Ombi-app/Ombi/commit/28e248046ad56390595f84172bbd5f5961325b4d))
## [4.27.2](https://github.com/Ombi-app/Ombi/compare/v4.27.1...v4.27.2) (2022-09-29)
### 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)) * **sonarr:** :bug: Cleaned up and removed Sonarr v3 option, sonarr v3 is now the default. This allows us to get ready for the upcoming Sonarr v4 ([#4764](https://github.com/Ombi-app/Ombi/issues/4764)) ([2cddec7](https://github.com/Ombi-app/Ombi/commit/2cddec759004b6490f686ff74cb092238e3dc946))
## [4.12.1](https://github.com/Ombi-app/Ombi/compare/v4.12.0...v4.12.1) (2022-02-16) ## [4.27.1](https://github.com/Ombi-app/Ombi/compare/v4.27.0...v4.27.1) (2022-09-20)
### 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:** stop the plex sync from deleting episodes when we can't find the plex key ([66b05e5](https://github.com/Ombi-app/Ombi/commit/66b05e5a85dbfe1fec5f9366e80987f2cfa1f4fe))
# [4.12.0](https://github.com/Ombi-app/Ombi/compare/v4.11.8...v4.12.0) (2022-02-14) # [4.27.0](https://github.com/Ombi-app/Ombi/compare/v4.26.0...v4.27.0) (2022-09-14)
### Features ### Features
* **radarr:** 4K Requests and Radarr 4K support ([ba88848](https://github.com/Ombi-app/Ombi/commit/ba88848866b0a9dedb1e79b55c4d81a0fd453843)) * Recently requested improvements ([#4755](https://github.com/Ombi-app/Ombi/issues/4755)) ([ff04d87](https://github.com/Ombi-app/Ombi/commit/ff04d875343604c77c391bf55d0968977e480281))
## [4.11.8](https://github.com/Ombi-app/Ombi/compare/v4.11.7...v4.11.8) (2022-02-13) # [4.26.0](https://github.com/Ombi-app/Ombi/compare/v4.25.1...v4.26.0) (2022-09-07)
### Features
* **notifications:** Add more curly variables for partially available notification ([66aa101](https://github.com/Ombi-app/Ombi/commit/66aa101019c4c4b34e186db9d303049d02b9c781))
## [4.25.1](https://github.com/Ombi-app/Ombi/compare/v4.25.0...v4.25.1) (2022-09-07)
### Bug Fixes ### 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)) * **webhook:** Remove added trailing slash from webhook URL [#4710](https://github.com/Ombi-app/Ombi/issues/4710) ([369eb33](https://github.com/Ombi-app/Ombi/commit/369eb339171671101be219486e2aab27a20f3d74))
## [4.11.7](https://github.com/Ombi-app/Ombi/compare/v4.11.6...v4.11.7) (2022-02-12) # [4.25.0](https://github.com/Ombi-app/Ombi/compare/v4.24.0...v4.25.0) (2022-08-23)
### 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)) * fixed stats controller ([#4742](https://github.com/Ombi-app/Ombi/issues/4742)) ([47ea64b](https://github.com/Ombi-app/Ombi/commit/47ea64b5a401770f1943b575ca40f84d515e96b3))
### Features
* Watchlist history errors([#4741](https://github.com/Ombi-app/Ombi/issues/4741)) ([c222f1a](https://github.com/Ombi-app/Ombi/commit/c222f1a945e944ef34e68cad2b61f40e57cab823))
# [4.24.0](https://github.com/Ombi-app/Ombi/compare/v4.23.2...v4.24.0) (2022-08-22)
### Features
* add crew on movie page ([#4722](https://github.com/Ombi-app/Ombi/issues/4722)) ([1d53261](https://github.com/Ombi-app/Ombi/commit/1d532613823804b25984bd1d223d081a54ad143d))
## [4.11.6](https://github.com/Ombi-app/Ombi/compare/v4.11.5...v4.11.6) (2022-02-10) ## [4.23.2](https://github.com/Ombi-app/Ombi/compare/v4.23.1...v4.23.2) (2022-08-22)
### Bug Fixes ### Bug Fixes
* **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)) * Fix conflicting property name for Swagger ([#4733](https://github.com/Ombi-app/Ombi/issues/4733)) ([d661f32](https://github.com/Ombi-app/Ombi/commit/d661f32e8a9e105faab6380b4b7b642896b98163))

@ -78,13 +78,6 @@ Here are some of the features Ombi has:
<sub><b>Jamie</b></sub> <sub><b>Jamie</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/Roxedus">
<img src="https://avatars.githubusercontent.com/u/7110194?v=4" width="50;" alt="Roxedus"/>
<br />
<sub><b>Roxedus</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/twanariens"> <a href="https://github.com/twanariens">
<img src="https://avatars.githubusercontent.com/u/34845004?v=4" width="50;" alt="twanariens"/> <img src="https://avatars.githubusercontent.com/u/34845004?v=4" width="50;" alt="twanariens"/>
@ -112,15 +105,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Anojh Thayaparan</b></sub> <sub><b>Anojh Thayaparan</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Magikarplvl4"> <a href="https://github.com/Magikarplvl4">
<img src="https://avatars.githubusercontent.com/u/2944704?v=4" width="50;" alt="Magikarplvl4"/> <img src="https://avatars.githubusercontent.com/u/2944704?v=4" width="50;" alt="Magikarplvl4"/>
<br /> <br />
<sub><b>Magikarp Lvl 4</b></sub> <sub><b>Magikarp Lvl 4</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/MrTopCat"> <a href="https://github.com/MrTopCat">
<img src="https://avatars.githubusercontent.com/u/774415?v=4" width="50;" alt="MrTopCat"/> <img src="https://avatars.githubusercontent.com/u/774415?v=4" width="50;" alt="MrTopCat"/>
@ -155,15 +148,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Dhruv Bhavsar</b></sub> <sub><b>Dhruv Bhavsar</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/joshuaboniface"> <a href="https://github.com/joshuaboniface">
<img src="https://avatars.githubusercontent.com/u/4031396?v=4" width="50;" alt="joshuaboniface"/> <img src="https://avatars.githubusercontent.com/u/4031396?v=4" width="50;" alt="joshuaboniface"/>
<br /> <br />
<sub><b>Joshua M. Boniface</b></sub> <sub><b>Joshua M. Boniface</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/bruvv"> <a href="https://github.com/bruvv">
<img src="https://avatars.githubusercontent.com/u/3063928?v=4" width="50;" alt="bruvv"/> <img src="https://avatars.githubusercontent.com/u/3063928?v=4" width="50;" alt="bruvv"/>
@ -198,15 +191,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Julien Loir</b></sub> <sub><b>Julien Loir</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/ProtoJazz"> <a href="https://github.com/ProtoJazz">
<img src="https://avatars.githubusercontent.com/u/1490293?v=4" width="50;" alt="ProtoJazz"/> <img src="https://avatars.githubusercontent.com/u/1490293?v=4" width="50;" alt="ProtoJazz"/>
<br /> <br />
<sub><b>Jim MacKenize</b></sub> <sub><b>Jim MacKenize</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Unimatrix0"> <a href="https://github.com/Unimatrix0">
<img src="https://avatars.githubusercontent.com/u/357984?v=4" width="50;" alt="Unimatrix0"/> <img src="https://avatars.githubusercontent.com/u/357984?v=4" width="50;" alt="Unimatrix0"/>
@ -241,15 +234,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Taylor Buchanan</b></sub> <sub><b>Taylor Buchanan</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/fservida"> <a href="https://github.com/fservida">
<img src="https://avatars.githubusercontent.com/u/501958?v=4" width="50;" alt="fservida"/> <img src="https://avatars.githubusercontent.com/u/501958?v=4" width="50;" alt="fservida"/>
<br /> <br />
<sub><b>Francesco Servida</b></sub> <sub><b>Francesco Servida</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Patricol"> <a href="https://github.com/Patricol">
<img src="https://avatars.githubusercontent.com/u/13428020?v=4" width="50;" alt="Patricol"/> <img src="https://avatars.githubusercontent.com/u/13428020?v=4" width="50;" alt="Patricol"/>
@ -257,6 +250,13 @@ Here are some of the features Ombi has:
<sub><b>Patrick Collins</b></sub> <sub><b>Patrick Collins</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/xweskingx">
<img src="https://avatars.githubusercontent.com/u/6268446?v=4" width="50;" alt="xweskingx"/>
<br />
<sub><b>Wesley King</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/chriscpritchard"> <a href="https://github.com/chriscpritchard">
<img src="https://avatars.githubusercontent.com/u/1839074?v=4" width="50;" alt="chriscpritchard"/> <img src="https://avatars.githubusercontent.com/u/1839074?v=4" width="50;" alt="chriscpritchard"/>
@ -515,6 +515,13 @@ Here are some of the features Ombi has:
<sub><b>Fish2</b></sub> <sub><b>Fish2</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/ketsapiwiq">
<img src="https://avatars.githubusercontent.com/u/26697460?v=4" width="50;" alt="ketsapiwiq"/>
<br />
<sub><b>Hadrien</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/hariesramdhani"> <a href="https://github.com/hariesramdhani">
<img src="https://avatars.githubusercontent.com/u/24251244?v=4" width="50;" alt="hariesramdhani"/> <img src="https://avatars.githubusercontent.com/u/24251244?v=4" width="50;" alt="hariesramdhani"/>
@ -535,15 +542,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Imgbot</b></sub> <sub><b>Imgbot</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/JPyke3"> <a href="https://github.com/JPyke3">
<img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="JPyke3"/> <img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="JPyke3"/>
<br /> <br />
<sub><b>Jacob Pyke</b></sub> <sub><b>Jacob Pyke</b></sub>
</a> </a>
</td></tr> </td>
<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"/>
@ -578,15 +585,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Jono Cairns</b></sub> <sub><b>Jono Cairns</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/krisklosterman"> <a href="https://github.com/krisklosterman">
<img src="https://avatars.githubusercontent.com/u/7139579?v=4" width="50;" alt="krisklosterman"/> <img src="https://avatars.githubusercontent.com/u/7139579?v=4" width="50;" alt="krisklosterman"/>
<br /> <br />
<sub><b>Kris Klosterman</b></sub> <sub><b>Kris Klosterman</b></sub>
</a> </a>
</td></tr> </td>
<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"/>
@ -594,6 +601,13 @@ Here are some of the features Ombi has:
<sub><b>Kyle Lucy</b></sub> <sub><b>Kyle Lucy</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/janderedev">
<img src="https://avatars.githubusercontent.com/u/26145882?v=4" width="50;" alt="janderedev"/>
<br />
<sub><b>Lea</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/Lixumos"> <a href="https://github.com/Lixumos">
<img src="https://avatars.githubusercontent.com/u/29160577?v=4" width="50;" alt="Lixumos"/> <img src="https://avatars.githubusercontent.com/u/29160577?v=4" width="50;" alt="Lixumos"/>
@ -614,7 +628,8 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Madeleine Schönemann</b></sub> <sub><b>Madeleine Schönemann</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/marleypowell"> <a href="https://github.com/marleypowell">
<img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/> <img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/>
@ -628,8 +643,7 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Matt</b></sub> <sub><b>Matt</b></sub>
</a> </a>
</td></tr> </td>
<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"/>
@ -644,13 +658,21 @@ Here are some of the features Ombi has:
<sub><b>Micky</b></sub> <sub><b>Micky</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/mvicomoya">
<img src="https://avatars.githubusercontent.com/u/24613599?v=4" width="50;" alt="mvicomoya"/>
<br />
<sub><b>Miguel A Vico Moya</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/beast3334"> <a href="https://github.com/beast3334">
<img src="https://avatars.githubusercontent.com/u/20631046?v=4" width="50;" alt="beast3334"/> <img src="https://avatars.githubusercontent.com/u/20631046?v=4" width="50;" alt="beast3334"/>
<br /> <br />
<sub><b>Nathan Miller</b></sub> <sub><b>Nathan Miller</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/cqxmzz"> <a href="https://github.com/cqxmzz">
<img src="https://avatars.githubusercontent.com/u/3071863?v=4" width="50;" alt="cqxmzz"/> <img src="https://avatars.githubusercontent.com/u/3071863?v=4" width="50;" alt="cqxmzz"/>
@ -658,21 +680,13 @@ Here are some of the features Ombi has:
<sub><b>Qiming Chen</b></sub> <sub><b>Qiming Chen</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/randallbruder">
<img src="https://avatars.githubusercontent.com/u/6447487?v=4" width="50;" alt="randallbruder"/>
<br />
<sub><b>Randall Bruder</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/rob1998"> <a href="https://github.com/rob1998">
<img src="https://avatars.githubusercontent.com/u/1560707?v=4" width="50;" alt="rob1998"/> <img src="https://avatars.githubusercontent.com/u/1560707?v=4" width="50;" alt="rob1998"/>
<br /> <br />
<sub><b>Rob Gökemeijer</b></sub> <sub><b>Rob Gökemeijer</b></sub>
</a> </a>
</td></tr> </td>
<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"/>
@ -700,7 +714,8 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Teifun2</b></sub> <sub><b>Teifun2</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<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"/>
@ -714,8 +729,7 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Tim Trott</b></sub> <sub><b>Tim Trott</b></sub>
</a> </a>
</td></tr> </td>
<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"/>
@ -743,7 +757,8 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Xirg</b></sub> <sub><b>Xirg</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/bazhip"> <a href="https://github.com/bazhip">
<img src="https://avatars.githubusercontent.com/u/10350445?v=4" width="50;" alt="bazhip"/> <img src="https://avatars.githubusercontent.com/u/10350445?v=4" width="50;" alt="bazhip"/>
@ -757,8 +772,7 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Blake Drumm</b></sub> <sub><b>Blake Drumm</b></sub>
</a> </a>
</td></tr> </td>
<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"/>
@ -786,7 +800,8 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Echel0n</b></sub> <sub><b>Echel0n</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<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"/>
@ -800,8 +815,7 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Maartenheebink</b></sub> <sub><b>Maartenheebink</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/masterhuck"> <a href="https://github.com/masterhuck">
<img src="https://avatars.githubusercontent.com/u/4671442?v=4" width="50;" alt="masterhuck"/> <img src="https://avatars.githubusercontent.com/u/4671442?v=4" width="50;" alt="masterhuck"/>
@ -829,7 +843,8 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Tdorsey</b></sub> <sub><b>Tdorsey</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/thegame3202"> <a href="https://github.com/thegame3202">
<img src="https://avatars.githubusercontent.com/u/22148848?v=4" width="50;" alt="thegame3202"/> <img src="https://avatars.githubusercontent.com/u/22148848?v=4" width="50;" alt="thegame3202"/>

File diff suppressed because one or more lines are too long

@ -268,6 +268,12 @@
<e p="cast-carousel.component.scss" t="Include" /> <e p="cast-carousel.component.scss" t="Include" />
<e p="cast-carousel.component.ts" t="Include" /> <e p="cast-carousel.component.ts" t="Include" />
</e> </e>
<e p="crew-carousel" t="Include">
<e p="crew-carousel.component.html" t="Include" />
<e p="crew-carousel.component.scss" t="Include" />
<e p="crew-carousel.component.ts" t="Include" />
</e>
<e p="deny-dialog" t="Include"> <e p="deny-dialog" t="Include">
<e p="deny-dialog.component.html" t="Include" /> <e p="deny-dialog.component.html" t="Include" />
<e p="deny-dialog.component.ts" t="Include" /> <e p="deny-dialog.component.ts" t="Include" />

@ -1,4 +1,6 @@
using System.Net.Http; using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ombi.Api.Discord.Models; using Ombi.Api.Discord.Models;
@ -12,18 +14,31 @@ namespace Ombi.Api.Discord
Api = api; Api = api;
} }
private const string BaseUrl = "https://discordapp.com/api/"; private const string _baseUrl = "https://discordapp.com/api/";
private IApi Api { get; } private IApi Api { get; }
public async Task SendMessage(DiscordWebhookBody body, string webhookId, string webhookToken) public async Task SendMessage(DiscordWebhookBody body, string webhookId, string webhookToken)
{ {
var request = new Request($"webhooks/{webhookId}/{webhookToken}", BaseUrl, HttpMethod.Post); var request = new Request($"webhooks/{webhookId}/{webhookToken}", _baseUrl, HttpMethod.Post);
request.AddJsonBody(body); request.AddJsonBody(body);
request.ApplicationJsonContentType(); request.ApplicationJsonContentType();
await Api.Request(request); var response = await Api.Request(request);
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new DiscordException(content, response.StatusCode);
}
}
public class DiscordException : Exception
{
public DiscordException(string content, HttpStatusCode code) : base($"Exception when calling Discord with status code {code} and message: {content}")
{
}
} }
} }
} }

@ -5,27 +5,16 @@ namespace Ombi.Api.Lidarr.Models
{ {
public class ArtistAdd public class ArtistAdd
{ {
public string status { get; set; }
public bool ended { get; set; }
public string artistName { get; set; } public string artistName { get; set; }
public string foreignArtistId { get; set; } public string foreignArtistId { get; set; }
public int tadbId { get; set; }
public int discogsId { get; set; }
public string overview { get; set; }
public string disambiguation { get; set; }
public Link[] links { get; set; } public Link[] links { get; set; }
public Image[] images { get; set; } public Image[] images { get; set; }
public string remotePoster { get; set; }
public int qualityProfileId { get; set; } public int qualityProfileId { get; set; }
public int metadataProfileId { get; set; } public int metadataProfileId { get; set; }
public bool albumFolder { get; set; } public bool albumFolder { get; set; }
public bool monitored { get; set; } public bool monitored { get; set; }
public string cleanName { get; set; } public string cleanName { get; set; }
public string sortName { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; } public DateTime added { get; set; }
public Ratings ratings { get; set; }
public Statistics statistics { get; set; }
public Addoptions addOptions { get; set; } public Addoptions addOptions { get; set; }
public string rootFolderPath { get; set; } public string rootFolderPath { get; set; }
} }
@ -49,4 +38,4 @@ namespace Ombi.Api.Lidarr.Models
None, None,
Unknown Unknown
} }
} }

@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -1,12 +1,10 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Hqub.MusicBrainz.API; using Hqub.MusicBrainz.API;
using Hqub.MusicBrainz.API.Entities; using Hqub.MusicBrainz.API.Entities;
using Hqub.MusicBrainz.API.Entities.Collections;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ombi.Api.MusicBrainz.Models; using Ombi.Api.MusicBrainz.Models;
@ -14,28 +12,30 @@ namespace Ombi.Api.MusicBrainz
{ {
public class MusicBrainzApi : IMusicBrainzApi public class MusicBrainzApi : IMusicBrainzApi
{ {
private readonly MusicBrainzClient _client;
private readonly IApi _api; private readonly IApi _api;
public MusicBrainzApi(IApi api) public MusicBrainzApi(MusicBrainzClient client, IApi api)
{ {
_client = client;
_api = api; _api = api;
} }
public Task<Release> GetAlbumInformation(string albumId) public Task<Release> GetAlbumInformation(string albumId)
{ {
var album = Release.GetAsync(albumId); var album = _client.Releases.GetAsync(albumId);
return album; return album;
} }
public async Task<IEnumerable<Artist>> SearchArtist(string artistQuery) public async Task<IEnumerable<Artist>> SearchArtist(string artistQuery)
{ {
var artist = await Artist.SearchAsync(artistQuery, 10); var artist = await _client.Artists.SearchAsync(artistQuery, 10);
return artist.Items.Where(x => x.Type != null); return artist.Items.Where(x => x.Type != null);
} }
public async Task<Artist> GetArtistInformation(string artistId) public async Task<Artist> GetArtistInformation(string artistId)
{ {
var artist = await Artist.GetAsync(artistId, "artist-rels", "url-rels", "releases", "release-groups"); var artist = await _client.Artists.GetAsync(artistId, "artist-rels", "url-rels", "releases", "release-groups");
return artist; return artist;
} }
@ -49,8 +49,7 @@ namespace Ombi.Api.MusicBrainz
}; };
// Search for a release by title. // Search for a release by title.
var releases = await Release.SearchAsync(query); var releases = await _client.Releases.SearchAsync(query);
return releases.Items; return releases.Items;
} }
@ -65,10 +64,5 @@ namespace Ombi.Api.MusicBrainz
} }
return null; return null;
} }
private void AddHeaders(Request req)
{
req.AddHeader("Accept", "application/json");
}
} }
} }

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MusicBrainzAPI" Version="2.0.1" /> <PackageReference Include="MusicBrainzAPI" Version="2.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.4.2" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -39,6 +39,7 @@ namespace Ombi.Api.Plex.Models
public string grandparentTheme { get; set; } public string grandparentTheme { get; set; }
public string chapterSource { get; set; } public string chapterSource { get; set; }
public Medium[] Media { get; set; } public Medium[] Media { get; set; }
[JsonProperty("Guid")] // force uppercase to solve conflict with lowercase guid
public List<PlexGuids> Guid { get; set; } = new List<PlexGuids>(); public List<PlexGuids> Guid { get; set; } = new List<PlexGuids>();
} }

@ -1,9 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ombi.Api.Plex.Models namespace Ombi.Api.Plex.Models
{ {
public class PlexWatchlistContainer public class PlexWatchlistContainer
{ {
public PlexWatchlist MediaContainer { get; set; } public PlexWatchlist MediaContainer { get; set; }
[JsonIgnore]
public bool AuthError { get; set; }
} }
} }

@ -1,4 +1,5 @@
using System; using System;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -295,9 +296,18 @@ namespace Ombi.Api.Plex
var request = new Request("library/sections/watchlist/all", WatchlistUri, HttpMethod.Get); var request = new Request("library/sections/watchlist/all", WatchlistUri, HttpMethod.Get);
await AddHeaders(request, plexToken); await AddHeaders(request, plexToken);
var result = await Api.Request<PlexWatchlistContainer>(request, cancellationToken); var result = await Api.Request(request, cancellationToken);
return result; if (result.StatusCode.Equals(HttpStatusCode.Unauthorized))
{
return new PlexWatchlistContainer
{
AuthError = true
};
}
var receivedString = await result.Content.ReadAsStringAsync(cancellationToken);
return JsonConvert.DeserializeObject<PlexWatchlistContainer>(receivedString);
} }
public async Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken) public async Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken)

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.Radarr.Models; using Ombi.Api.Radarr.Models;
using Ombi.Api.Radarr.Models.V3; using Ombi.Api.Radarr.Models.V3;
@ -14,7 +15,8 @@ namespace Ombi.Api.Radarr
Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl); Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl);
Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl); Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl);
Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl); Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl);
Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability); Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability, List<int> tags);
Task<List<Tag>> GetTags(string apiKey, string baseUrl); Task<List<Tag>> GetTags(string apiKey, string baseUrl);
Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName);
} }
} }

@ -29,5 +29,6 @@ namespace Ombi.Api.Radarr.Models
public int year { get; set; } public int year { get; set; }
public string minimumAvailability { get; set; } public string minimumAvailability { get; set; }
public long sizeOnDisk { get; set; } public long sizeOnDisk { get; set; }
public int[] tags { get; set; }
} }
} }

@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -72,7 +72,7 @@ namespace Ombi.Api.Radarr
return await Api.Request<MovieResponse>(request); return await Api.Request<MovieResponse>(request);
} }
public async Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability) public async Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability, List<int> tags)
{ {
var request = new Request("/api/v3/movie", baseUrl, HttpMethod.Post); var request = new Request("/api/v3/movie", baseUrl, HttpMethod.Post);
@ -86,7 +86,8 @@ namespace Ombi.Api.Radarr
monitored = true, monitored = true,
year = year, year = year,
minimumAvailability = minimumAvailability, minimumAvailability = minimumAvailability,
sizeOnDisk = 0 sizeOnDisk = 0,
tags = tags.Any() ? tags.ToArray() : Enumerable.Empty<int>().ToArray()
}; };
if (searchNow) if (searchNow)
@ -156,5 +157,14 @@ namespace Ombi.Api.Radarr
{ {
request.AddHeader("X-Api-Key", key); request.AddHeader("X-Api-Key", key);
} }
public Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName)
{
var request = new Request($"/api/v3/tag", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(new { Label = tagName });
return Api.Request<Tag>(request);
}
} }
} }

@ -1,7 +1,6 @@
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;
using System.Net.Http;
using Ombi.Api.Sonarr.Models.V3; using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr namespace Ombi.Api.Sonarr
@ -9,5 +8,8 @@ namespace Ombi.Api.Sonarr
public interface ISonarrV3Api : ISonarrApi public interface ISonarrV3Api : ISonarrApi
{ {
Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl); Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl);
Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName);
Task<Tag> GetTag(int tagId, string apiKey, string baseUrl);
Task<List<MonitoredEpisodeResult>> MonitorEpisode(int[] episodeIds, bool monitor, string apiKey, string baseUrl);
} }
} }

@ -8,7 +8,7 @@ namespace Ombi.Api.Sonarr.Models
{ {
public Episode() public Episode()
{ {
} }
public Episode(Episode ep) public Episode(Episode ep)
@ -53,7 +53,7 @@ namespace Ombi.Api.Sonarr.Models
{ {
public Episodefile() public Episodefile()
{ {
} }
public Episodefile(Episodefile e) public Episodefile(Episodefile e)
@ -85,7 +85,7 @@ namespace Ombi.Api.Sonarr.Models
{ {
public EpisodeQuality() public EpisodeQuality()
{ {
} }
public EpisodeQuality(EpisodeQuality e) public EpisodeQuality(EpisodeQuality e)
@ -101,7 +101,7 @@ namespace Ombi.Api.Sonarr.Models
{ {
public Revision() public Revision()
{ {
} }
public Revision(Revision r) public Revision(Revision r)
@ -113,6 +113,17 @@ namespace Ombi.Api.Sonarr.Models
public int real { get; set; } public int real { get; set; }
} }
public class MonitoredEpisodeResult
{
public int seriesId { get; set; }
public int tvdbId { get; set; }
public int episodeFileId { get; set; }
public int seasonNumber { get; set; }
public int episodeNumber { get; set; }
public string overview { get; set; }
public bool monitored { get; set; }
public int id { get; set; }
}
public class EpisodeUpdateResult public class EpisodeUpdateResult
{ {

@ -26,6 +26,7 @@ namespace Ombi.Api.Sonarr.Models
public string seriesType { get; set; } public string seriesType { get; set; }
public int id { get; set; } public int id { get; set; }
public List<SonarrImage> images { get; set; } public List<SonarrImage> images { get; set; }
public List<int> tags { get; set; }
// V3 Property // V3 Property
public int languageProfileId { get; set; } public int languageProfileId { get; set; }

@ -5,8 +5,6 @@ namespace Ombi.Api.Sonarr.Models
public class SonarrProfile public class SonarrProfile
{ {
public string name { get; set; } public string name { get; set; }
public Cutoff cutoff { get; set; }
public List<Item> items { get; set; }
public int id { get; set; } public int id { get; set; }
} }
} }

@ -40,7 +40,7 @@ namespace Ombi.Api.Sonarr.Models
public string titleSlug { get; set; } public string titleSlug { get; set; }
public string certification { get; set; } public string certification { get; set; }
public string[] genres { get; set; } public string[] genres { get; set; }
public object[] tags { get; set; } public List<int> tags { get; set; }
public DateTime added { get; set; } public DateTime added { get; set; }
public Ratings ratings { get; set; } public Ratings ratings { get; set; }
public int qualityProfileId { get; set; } public int qualityProfileId { get; set; }

@ -11,7 +11,6 @@ namespace Ombi.Api.Sonarr
{ {
public SonarrV3Api(IApi api) : base(api) public SonarrV3Api(IApi api) : base(api)
{ {
} }
protected override string ApiBaseUrl => "/api/v3/"; protected override string ApiBaseUrl => "/api/v3/";
@ -30,5 +29,30 @@ namespace Ombi.Api.Sonarr
request.AddHeader("X-Api-Key", apiKey); request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrProfile>>(request); return await Api.Request<List<SonarrProfile>>(request);
} }
public Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName)
{
var request = new Request($"{ApiBaseUrl}tag", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(new { Label = tagName });
return Api.Request<Tag>(request);
}
public Task<Tag> GetTag(int tagId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}tag/{tagId}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return Api.Request<Tag>(request);
}
public async Task<List<MonitoredEpisodeResult>> MonitorEpisode(int[] episodeIds, bool monitor, string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}Episode/monitor", baseUrl, HttpMethod.Put);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(new { episodeIds = episodeIds, monitored = monitor });
return await Api.Request<List<MonitoredEpisodeResult>>(request);
}
} }
} }

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Twilio" Version="5.37.2" /> <PackageReference Include="Twilio" Version="5.80.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -19,8 +19,8 @@ namespace Ombi.Api.Webhook
public async Task PushAsync(string baseUrl, string accessToken, IDictionary<string, string> parameters) public async Task PushAsync(string baseUrl, string accessToken, IDictionary<string, string> parameters)
{ {
var request = new Request("", baseUrl, HttpMethod.Post); var request = new Request("", baseUrl, HttpMethod.Post) {IgnoreBaseUrlAppend = true};
if (!string.IsNullOrWhiteSpace(accessToken)) if (!string.IsNullOrWhiteSpace(accessToken))
{ {
request.AddHeader("Access-Token", accessToken); request.AddHeader("Access-Token", accessToken);

@ -12,9 +12,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Polly" Version="7.1.0" /> <PackageReference Include="Polly" Version="7.2.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -2,7 +2,6 @@
using Moq; using Moq;
using Moq.AutoMock; using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Engine; using Ombi.Core.Engine;
using Ombi.Core.Helpers; using Ombi.Core.Helpers;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
@ -52,7 +51,7 @@ namespace Ombi.Core.Tests.Engine
_subject = _mocker.CreateInstance<MovieRequestEngine>(); _subject = _mocker.CreateInstance<MovieRequestEngine>();
var list = DbHelper.GetQueryableMockDbSet(new RequestSubscription()); var list = DbHelper.GetQueryableMockDbSet(new RequestSubscription());
_mocker.Setup<IRepository<RequestSubscription>, IQueryable<RequestSubscription>>(x => x.GetAll()).Returns(new List<RequestSubscription>().AsQueryable().BuildMock().Object); _mocker.Setup<IRepository<RequestSubscription>, IQueryable<RequestSubscription>>(x => x.GetAll()).Returns(new List<RequestSubscription>().AsQueryable().BuildMock());
} }
[Test] [Test]

@ -3,7 +3,6 @@ using Moq;
using Moq.AutoMock; using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers; using Ombi.Core.Helpers;
using Ombi.Core.Models; using Ombi.Core.Models;
using Ombi.Core.Services; using Ombi.Core.Services;
@ -64,7 +63,7 @@ namespace Ombi.Core.Tests.Engine
var user = new OmbiUser(); var user = new OmbiUser();
var um = _mocker.GetMock<OmbiUserManager>(); var um = _mocker.GetMock<OmbiUserManager>();
um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock().Object); um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock());
@ -82,7 +81,7 @@ namespace Ombi.Core.Tests.Engine
}; };
var um = _mocker.GetMock<OmbiUserManager>(); var um = _mocker.GetMock<OmbiUserManager>();
um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock().Object); um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock());
@ -100,7 +99,7 @@ namespace Ombi.Core.Tests.Engine
MovieRequestLimit = 1 MovieRequestLimit = 1
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(new List<RequestLog>().AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(new List<RequestLog>().AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -131,7 +130,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -206,7 +205,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -239,7 +238,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -272,7 +271,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -311,7 +310,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -344,7 +343,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user); var result = await _subject.GetRemainingMovieRequests(user);
@ -376,7 +375,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user, today); var result = await _subject.GetRemainingMovieRequests(user, today);
@ -415,7 +414,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user, today); var result = await _subject.GetRemainingMovieRequests(user, today);
@ -448,7 +447,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user, today); var result = await _subject.GetRemainingMovieRequests(user, today);
@ -481,7 +480,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user, today); var result = await _subject.GetRemainingMovieRequests(user, today);
@ -521,7 +520,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMovieRequests(user, today); var result = await _subject.GetRemainingMovieRequests(user, today);

@ -3,7 +3,6 @@ using Moq;
using Moq.AutoMock; using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers; using Ombi.Core.Helpers;
using Ombi.Core.Models; using Ombi.Core.Models;
using Ombi.Core.Services; using Ombi.Core.Services;
@ -62,7 +61,7 @@ namespace Ombi.Core.Tests.Engine
var user = new OmbiUser(); var user = new OmbiUser();
var um = _mocker.GetMock<OmbiUserManager>(); var um = _mocker.GetMock<OmbiUserManager>();
um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock().Object); um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock());
@ -80,7 +79,7 @@ namespace Ombi.Core.Tests.Engine
}; };
var um = _mocker.GetMock<OmbiUserManager>(); var um = _mocker.GetMock<OmbiUserManager>();
um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock().Object); um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock());
@ -98,7 +97,7 @@ namespace Ombi.Core.Tests.Engine
MusicRequestLimit = 1 MusicRequestLimit = 1
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(new List<RequestLog>().AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(new List<RequestLog>().AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -129,7 +128,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -204,7 +203,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -237,7 +236,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -270,7 +269,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -309,7 +308,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -342,7 +341,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -374,7 +373,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user, today); var result = await _subject.GetRemainingMusicRequests(user, today);
@ -413,7 +412,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -445,7 +444,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -478,7 +477,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user); var result = await _subject.GetRemainingMusicRequests(user);
@ -518,7 +517,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingMusicRequests(user, today); var result = await _subject.GetRemainingMusicRequests(user, today);

@ -3,7 +3,6 @@ using Moq;
using Moq.AutoMock; using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers; using Ombi.Core.Helpers;
using Ombi.Core.Models; using Ombi.Core.Models;
using Ombi.Core.Services; using Ombi.Core.Services;
@ -59,7 +58,7 @@ namespace Ombi.Core.Tests.Engine
var user = new OmbiUser(); var user = new OmbiUser();
var um = _mocker.GetMock<OmbiUserManager>(); var um = _mocker.GetMock<OmbiUserManager>();
um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock().Object); um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock());
@ -77,7 +76,7 @@ namespace Ombi.Core.Tests.Engine
}; };
var um = _mocker.GetMock<OmbiUserManager>(); var um = _mocker.GetMock<OmbiUserManager>();
um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock().Object); um.SetupGet(x => x.Users).Returns(new List<OmbiUser> { user }.AsQueryable().BuildMock());
@ -95,7 +94,7 @@ namespace Ombi.Core.Tests.Engine
EpisodeRequestLimit = 1 EpisodeRequestLimit = 1
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(new List<RequestLog>().AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(new List<RequestLog>().AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -126,7 +125,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -209,7 +208,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -243,7 +242,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -276,7 +275,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -317,7 +316,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -358,7 +357,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -380,7 +379,7 @@ namespace Ombi.Core.Tests.Engine
EpisodeRequestLimitType = RequestLimitType.Week, EpisodeRequestLimitType = RequestLimitType.Week,
Id = "id1" Id = "id1"
}; };
var lastWeek = DateTime.Now.AddDays(-8); var lastWeek = DateTime.UtcNow.AddDays(-8);
var log = new List<RequestLog> var log = new List<RequestLog>
{ {
new RequestLog new RequestLog
@ -392,7 +391,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -425,7 +424,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -466,7 +465,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -507,7 +506,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -541,7 +540,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -574,7 +573,7 @@ namespace Ombi.Core.Tests.Engine
} }
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -615,7 +614,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);
@ -656,7 +655,7 @@ namespace Ombi.Core.Tests.Engine
}, },
}; };
var repoMock = _mocker.GetMock<IRepository<RequestLog>>(); var repoMock = _mocker.GetMock<IRepository<RequestLog>>();
repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock().Object); repoMock.Setup(x => x.GetAll()).Returns(log.AsQueryable().BuildMock());
var result = await _subject.GetRemainingTvRequests(user); var result = await _subject.GetRemainingTvRequests(user);

@ -1,6 +1,5 @@
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;
using AutoFixture; using AutoFixture;
using MockQueryable.Moq; using MockQueryable.Moq;
@ -71,7 +70,7 @@ namespace Ombi.Core.Tests.Engine
VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes) VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes)
.AsQueryable() .AsQueryable()
.BuildMock().Object); .BuildMock());
var result = new VoteEngineResult(); var result = new VoteEngineResult();
if (type == VoteType.Downvote) if (type == VoteType.Downvote)
{ {
@ -118,7 +117,7 @@ namespace Ombi.Core.Tests.Engine
}); });
VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes) VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes)
.AsQueryable() .AsQueryable()
.BuildMock().Object); .BuildMock());
var result = new VoteEngineResult(); var result = new VoteEngineResult();
if (type == VoteType.Downvote) if (type == VoteType.Downvote)
{ {
@ -163,7 +162,7 @@ namespace Ombi.Core.Tests.Engine
}); });
VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes) VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes)
.AsQueryable() .AsQueryable()
.BuildMock().Object); .BuildMock());
var result = new VoteEngineResult(); var result = new VoteEngineResult();
if (type == VoteType.Downvote) if (type == VoteType.Downvote)

@ -8,18 +8,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" /> <PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="Moq" Version="4.15.1" /> <PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Moq.AutoMock" Version="3.0.0" /> <PackageReference Include="Moq.AutoMock" Version="3.4.0" />
<PackageReference Include="Nunit" Version="3.12.0" /> <PackageReference Include="Nunit" Version="3.13.3" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.11.1" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.15.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1"> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.8.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"></packagereference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -1,21 +1,15 @@
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;
using MockQueryable.Moq; using MockQueryable.Moq;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Rule.Rules;
using Ombi.Core.Rule.Rules.Request; using Ombi.Core.Rule.Rules.Request;
using Ombi.Core.Services; using Ombi.Core.Services;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities; 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 Ombi.Test.Common;
namespace Ombi.Core.Tests.Rule.Request namespace Ombi.Core.Tests.Rule.Request
{ {
@ -45,7 +39,7 @@ namespace Ombi.Core.Tests.Rule.Request
TheMovieDbId = 1, TheMovieDbId = 1,
RequestType = RequestType.Movie RequestType = RequestType.Movie
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var o = new MovieRequests var o = new MovieRequests
{ {
TheMovieDbId = 1, TheMovieDbId = 1,
@ -67,7 +61,7 @@ namespace Ombi.Core.Tests.Rule.Request
ImdbId = 1.ToString(), ImdbId = 1.ToString(),
RequestType = RequestType.Movie RequestType = RequestType.Movie
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var o = new MovieRequests var o = new MovieRequests
{ {
ImdbId = 1.ToString(), ImdbId = 1.ToString(),
@ -89,7 +83,7 @@ namespace Ombi.Core.Tests.Rule.Request
ImdbId = "2", ImdbId = "2",
RequestType = RequestType.Movie RequestType = RequestType.Movie
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var o = new MovieRequests var o = new MovieRequests
{ {
TheMovieDbId = 1, TheMovieDbId = 1,
@ -113,7 +107,7 @@ namespace Ombi.Core.Tests.Rule.Request
RequestType = RequestType.Movie, RequestType = RequestType.Movie,
Is4kRequest = true Is4kRequest = true
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var o = new MovieRequests var o = new MovieRequests
{ {
TheMovieDbId = 2, TheMovieDbId = 2,
@ -139,7 +133,7 @@ namespace Ombi.Core.Tests.Rule.Request
RequestType = RequestType.Movie, RequestType = RequestType.Movie,
Is4kRequest = false Is4kRequest = false
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var o = new MovieRequests var o = new MovieRequests
{ {
TheMovieDbId = 2, TheMovieDbId = 2,
@ -165,7 +159,7 @@ namespace Ombi.Core.Tests.Rule.Request
RequestType = RequestType.Movie, RequestType = RequestType.Movie,
Is4kRequest = false Is4kRequest = false
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var o = new MovieRequests var o = new MovieRequests
{ {
TheMovieDbId = 2, TheMovieDbId = 2,

@ -7,10 +7,8 @@ using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests; using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ombi.Core.Tests.Rule.Request namespace Ombi.Core.Tests.Rule.Request
@ -31,7 +29,7 @@ namespace Ombi.Core.Tests.Rule.Request
[Test] [Test]
public async Task RequestShow_DoesNotExistAtAll_IsSuccessful() public async Task RequestShow_DoesNotExistAtAll_IsSuccessful()
{ {
PlexContentRepo.Setup(x => x.GetAll()).Returns(new List<PlexServerContent>().AsQueryable().BuildMock().Object); PlexContentRepo.Setup(x => x.GetAll()).Returns(new List<PlexServerContent>().AsQueryable().BuildMock());
var req = new ChildRequests var req = new ChildRequests
{ {
SeasonRequests = new List<SeasonRequests> SeasonRequests = new List<SeasonRequests>
@ -203,7 +201,7 @@ namespace Ombi.Core.Tests.Rule.Request
TheMovieDbId = 123.ToString(), TheMovieDbId = 123.ToString(),
} }
}; };
PlexContentRepo.Setup(x => x.GetAll()).Returns(content.AsQueryable().BuildMock().Object); PlexContentRepo.Setup(x => x.GetAll()).Returns(content.AsQueryable().BuildMock());
var req = new MovieRequests var req = new MovieRequests
{ {
@ -245,7 +243,7 @@ namespace Ombi.Core.Tests.Rule.Request
} }
} }
}; };
PlexContentRepo.Setup(x => x.GetAll()).Returns(childRequests.AsQueryable().BuildMock().Object); PlexContentRepo.Setup(x => x.GetAll()).Returns(childRequests.AsQueryable().BuildMock());
} }
} }
} }

@ -5,10 +5,8 @@ using Ombi.Core.Rule.Rules.Request;
using Ombi.Store.Entities; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ombi.Core.Tests.Rule.Request namespace Ombi.Core.Tests.Rule.Request
@ -29,7 +27,7 @@ namespace Ombi.Core.Tests.Rule.Request
[Test] [Test]
public async Task RequestShow_DoesNotExistAtAll_IsSuccessful() public async Task RequestShow_DoesNotExistAtAll_IsSuccessful()
{ {
TvRequestRepo.Setup(x => x.GetChild()).Returns(new List<ChildRequests>().AsQueryable().BuildMock().Object); TvRequestRepo.Setup(x => x.GetChild()).Returns(new List<ChildRequests>().AsQueryable().BuildMock());
var req = new ChildRequests var req = new ChildRequests
{ {
SeasonRequests = new List<SeasonRequests> SeasonRequests = new List<SeasonRequests>
@ -209,7 +207,7 @@ namespace Ombi.Core.Tests.Rule.Request
} }
} }
}; };
TvRequestRepo.Setup(x => x.GetChild()).Returns(childRequests.AsQueryable().BuildMock().Object); TvRequestRepo.Setup(x => x.GetChild()).Returns(childRequests.AsQueryable().BuildMock());
} }
} }
} }

@ -54,7 +54,7 @@ namespace Ombi.Core.Tests.Senders
Id = "a", Id = "a",
Email = "Test@test.com" Email = "Test@test.com"
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model); var result = await _subject.SendMassEmail(model);
@ -95,7 +95,7 @@ namespace Ombi.Core.Tests.Senders
Id = "b", Id = "b",
Email = "b@test.com" Email = "b@test.com"
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model); var result = await _subject.SendMassEmail(model);
@ -129,7 +129,7 @@ namespace Ombi.Core.Tests.Senders
{ {
Id = "a", Id = "a",
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model); var result = await _subject.SendMassEmail(model);
_mocker.Verify<ILogger<MassEmailSender>>( _mocker.Verify<ILogger<MassEmailSender>>(
@ -177,7 +177,7 @@ namespace Ombi.Core.Tests.Senders
Id = "b", Id = "b",
Email = "b@test.com" Email = "b@test.com"
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model); var result = await _subject.SendMassEmail(model);
@ -217,7 +217,7 @@ namespace Ombi.Core.Tests.Senders
{ {
Id = "b", Id = "b",
} }
}.AsQueryable().BuildMock().Object); }.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model); var result = await _subject.SendMassEmail(model);

@ -0,0 +1,146 @@
using MockQueryable.Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Models;
using Ombi.Core.Services;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Test.Common;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UserType = Ombi.Store.Entities.UserType;
namespace Ombi.Core.Tests.Services
{
public class PlexServiceTests
{
private PlexService _subject;
private AutoMocker _mocker;
[SetUp]
public void Setup()
{
_mocker = new AutoMocker();
_subject = _mocker.CreateInstance<PlexService>();
}
[Test]
public async Task GetWatchListUsers_AllUsersSynced()
{
var userMock = MockHelper.MockUserManager(new List<OmbiUser>
{
new OmbiUser
{
MediaServerToken = "token",
Id = "1",
UserName = "user1",
UserType = UserType.PlexUser,
},
new OmbiUser
{
MediaServerToken = "token",
Id = "2",
UserName = "user2",
UserType = UserType.PlexUser,
},
new OmbiUser
{
MediaServerToken = "token",
Id = "2",
UserName = "user2",
UserType = UserType.LocalUser,
}
});
_mocker.Use(userMock.Object);
_subject = _mocker.CreateInstance<PlexService>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll())
.Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
var result = await _subject.GetWatchlistUsers(CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result.All(x => x.SyncStatus == WatchlistSyncStatus.Successful));
Assert.That(result.Count, Is.EqualTo(2));
});
}
[Test]
public async Task GetWatchListUsers_NotEnabled()
{
var userMock = MockHelper.MockUserManager(new List<OmbiUser>
{
new OmbiUser
{
MediaServerToken = "",
Id = "1",
UserName = "user1",
UserType = UserType.PlexUser,
},
new OmbiUser
{
MediaServerToken = null,
Id = "2",
UserName = "user2",
UserType = UserType.PlexUser,
},
});
_mocker.Use(userMock.Object);
_subject = _mocker.CreateInstance<PlexService>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll())
.Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
var result = await _subject.GetWatchlistUsers(CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result.All(x => x.SyncStatus == WatchlistSyncStatus.NotEnabled));
Assert.That(result.Count, Is.EqualTo(2));
});
}
[Test]
public async Task GetWatchListUsers_Failed()
{
var userMock = MockHelper.MockUserManager(new List<OmbiUser>
{
new OmbiUser
{
MediaServerToken = "test",
Id = "1",
UserName = "user1",
UserType = UserType.PlexUser,
},
});
_mocker.Use(userMock.Object);
_subject = _mocker.CreateInstance<PlexService>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll())
.Returns(new List<PlexWatchlistUserError>
{
new PlexWatchlistUserError
{
UserId = "1",
MediaServerToken = "test",
}
}.AsQueryable().BuildMock());
var result = await _subject.GetWatchlistUsers(CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result.All(x => x.SyncStatus == WatchlistSyncStatus.Failed));
Assert.That(result.Count, Is.EqualTo(1));
});
}
}
}

@ -0,0 +1,206 @@
using AutoFixture;
using MockQueryable.Moq;
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Helpers;
using Ombi.Core.Models.Requests;
using Ombi.Core.Services;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core.Tests.Services
{
[TestFixture]
public class RecentlyRequestedServiceTests
{
private AutoMocker _mocker;
private RecentlyRequestedService _subject;
private Fixture _fixture;
[SetUp]
public void Setup()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mocker = new AutoMocker();
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Alias = "alias", Language = "en" });
_mocker.Setup<ICurrentUser, string>(x => x.Username).Returns("test");
_subject = _mocker.CreateInstance<RecentlyRequestedService>();
}
[Test]
public async Task GetRecentlyRequested_Movies()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings());
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = new List<MovieRequests>
{
new MovieRequests
{
Id = 1,
Approved = true,
Available = true,
ReleaseDate = releaseDate,
Title = "title",
Overview = "overview",
RequestedDate = requestDate,
RequestedUser = new Store.Entities.OmbiUser
{
UserName = "a"
},
RequestedUserId = "b",
}
};
var albums = new List<AlbumRequest>();
var chilRequests = new List<ChildRequests>();
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock());
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock());
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock());
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First(), Is.InstanceOf<RecentlyRequestedModel>()
.With.Property(nameof(RecentlyRequestedModel.RequestId)).EqualTo(1)
.With.Property(nameof(RecentlyRequestedModel.Approved)).EqualTo(true)
.With.Property(nameof(RecentlyRequestedModel.Available)).EqualTo(true)
.With.Property(nameof(RecentlyRequestedModel.Title)).EqualTo("title")
.With.Property(nameof(RecentlyRequestedModel.Overview)).EqualTo("overview")
.With.Property(nameof(RecentlyRequestedModel.RequestDate)).EqualTo(requestDate)
.With.Property(nameof(RecentlyRequestedModel.ReleaseDate)).EqualTo(releaseDate)
.With.Property(nameof(RecentlyRequestedModel.Type)).EqualTo(RequestType.Movie)
);
}
[Test]
public async Task GetRecentlyRequested_Movies_HideAvailable()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings() { HideAvailableRecentlyRequested = true });
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = new List<MovieRequests>
{
new MovieRequests
{
Id = 1,
Approved = true,
Available = true,
ReleaseDate = releaseDate,
Title = "title",
Overview = "overview",
RequestedDate = requestDate,
RequestedUser = new Store.Entities.OmbiUser
{
UserName = "a"
},
RequestedUserId = "b",
},
new MovieRequests
{
Id = 1,
Approved = true,
Available = false,
ReleaseDate = releaseDate,
Title = "title2",
Overview = "overview2",
RequestedDate = requestDate,
RequestedUser = new Store.Entities.OmbiUser
{
UserName = "a"
},
RequestedUserId = "b",
}
};
var albums = new List<AlbumRequest>();
var chilRequests = new List<ChildRequests>();
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock());
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock());
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock());
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First(), Is.InstanceOf<RecentlyRequestedModel>()
.With.Property(nameof(RecentlyRequestedModel.RequestId)).EqualTo(1)
.With.Property(nameof(RecentlyRequestedModel.Approved)).EqualTo(true)
.With.Property(nameof(RecentlyRequestedModel.Available)).EqualTo(false)
.With.Property(nameof(RecentlyRequestedModel.Title)).EqualTo("title2")
.With.Property(nameof(RecentlyRequestedModel.Overview)).EqualTo("overview2")
.With.Property(nameof(RecentlyRequestedModel.RequestDate)).EqualTo(requestDate)
.With.Property(nameof(RecentlyRequestedModel.ReleaseDate)).EqualTo(releaseDate)
.With.Property(nameof(RecentlyRequestedModel.Type)).EqualTo(RequestType.Movie)
);
}
[Test]
public async Task GetRecentlyRequested()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings());
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = _fixture.CreateMany<MovieRequests>(10);
var albums = _fixture.CreateMany<AlbumRequest>(10);
var chilRequests = _fixture.CreateMany<ChildRequests>(10);
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock());
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock());
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock());
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.That(result.Count, Is.EqualTo(21));
}
[Test]
public async Task GetRecentlyRequested_HideUsernames()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings());
_mocker.Setup<ISettingsService<OmbiSettings>, Task<OmbiSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new OmbiSettings { HideRequestsUsers = true });
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = _fixture.CreateMany<MovieRequests>(10).ToList();
var albums = _fixture.CreateMany<AlbumRequest>(10);
var chilRequests = _fixture.CreateMany<ChildRequests>(10);
movies.Add(_fixture.Build<MovieRequests>().With(x => x.RequestedUserId, "a").With(x => x.Title, "unit").Create());
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock());
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock());
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock());
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Id = "a", Alias = "alias", UserType = UserType.LocalUser });
_mocker.Setup<ICurrentUser, string>(x => x.Username).Returns("test");
_mocker.Setup<OmbiUserManager, Task<bool>>(x => x.IsInRoleAsync(It.IsAny<OmbiUser>(), It.IsAny<string>())).ReturnsAsync(false);
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First().Title, Is.EqualTo("unit"));
});
}
}
}

@ -111,11 +111,11 @@ namespace Ombi.Core.Engine
if (model.Is4kRequest) if (model.Is4kRequest)
{ {
existingRequest.Is4kRequest = true; existingRequest.Is4kRequest = true;
existingRequest.RequestedDate4k = DateTime.Now; existingRequest.RequestedDate4k = DateTime.UtcNow;
} }
else else
{ {
existingRequest.RequestedDate = DateTime.Now; existingRequest.RequestedDate = DateTime.UtcNow;
} }
isExisting = true; isExisting = true;
requestModel = existingRequest; requestModel = existingRequest;
@ -134,7 +134,7 @@ namespace Ombi.Core.Engine
? DateTime.Parse(movieInfo.ReleaseDate) ? DateTime.Parse(movieInfo.ReleaseDate)
: DateTime.MinValue, : DateTime.MinValue,
Status = movieInfo.Status, Status = movieInfo.Status,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.Now, RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.UtcNow,
Approved = false, Approved = false,
Approved4K = false, Approved4K = false,
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id, RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
@ -143,7 +143,7 @@ namespace Ombi.Core.Engine
RequestedByAlias = model.RequestedByAlias, RequestedByAlias = model.RequestedByAlias,
RootPathOverride = model.RootFolderOverride.GetValueOrDefault(), RootPathOverride = model.RootFolderOverride.GetValueOrDefault(),
QualityOverride = model.QualityPathOverride.GetValueOrDefault(), QualityOverride = model.QualityPathOverride.GetValueOrDefault(),
RequestedDate4k = model.Is4kRequest ? DateTime.Now : DateTime.MinValue, RequestedDate4k = model.Is4kRequest ? DateTime.UtcNow : DateTime.MinValue,
Is4kRequest = model.Is4kRequest, Is4kRequest = model.Is4kRequest,
Source = model.Source Source = model.Source
}; };

@ -26,6 +26,7 @@ namespace Ombi.Core.Engine
private readonly IMusicRequestRepository _musicRepository; private readonly IMusicRequestRepository _musicRepository;
private readonly IRepository<Votes> _voteRepository; private readonly IRepository<Votes> _voteRepository;
private readonly IRepository<MobileDevices> _mobileDevicesRepository; private readonly IRepository<MobileDevices> _mobileDevicesRepository;
private readonly IRepository<PlexWatchlistUserError> _watchlistUserError;
public UserDeletionEngine(IMovieRequestRepository movieRepository, public UserDeletionEngine(IMovieRequestRepository movieRepository,
OmbiUserManager userManager, OmbiUserManager userManager,
@ -39,7 +40,8 @@ namespace Ombi.Core.Engine
IRepository<UserNotificationPreferences> notificationPreferencesRepo, IRepository<UserNotificationPreferences> notificationPreferencesRepo,
IRepository<UserQualityProfiles> qualityProfilesRepo, IRepository<UserQualityProfiles> qualityProfilesRepo,
IRepository<Votes> voteRepository, IRepository<Votes> voteRepository,
IRepository<MobileDevices> mobileDevicesRepository IRepository<MobileDevices> mobileDevicesRepository,
IRepository<PlexWatchlistUserError> watchlistUserError
) )
{ {
_movieRepository = movieRepository; _movieRepository = movieRepository;
@ -56,6 +58,7 @@ namespace Ombi.Core.Engine
_userQualityProfiles = qualityProfilesRepo; _userQualityProfiles = qualityProfilesRepo;
_voteRepository = voteRepository; _voteRepository = voteRepository;
_mobileDevicesRepository = mobileDevicesRepository; _mobileDevicesRepository = mobileDevicesRepository;
_watchlistUserError = watchlistUserError;
} }
@ -68,6 +71,7 @@ namespace Ombi.Core.Engine
var musicRequested = _musicRepository.GetAll().Where(x => x.RequestedUserId == userId); var musicRequested = _musicRepository.GetAll().Where(x => x.RequestedUserId == userId);
var notificationPreferences = _userNotificationPreferences.GetAll().Where(x => x.UserId == userId); var notificationPreferences = _userNotificationPreferences.GetAll().Where(x => x.UserId == userId);
var userQuality = await _userQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == userId); var userQuality = await _userQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == userId);
var watchlistError = await _watchlistUserError.GetAll().FirstOrDefaultAsync(x => x.UserId == userId);
if (moviesUserRequested.Any()) if (moviesUserRequested.Any())
{ {
@ -89,6 +93,10 @@ namespace Ombi.Core.Engine
{ {
await _userQualityProfiles.Delete(userQuality); await _userQualityProfiles.Delete(userQuality);
} }
if (watchlistError != null)
{
await _watchlistUserError.Delete(watchlistError);
}
// Delete any issues and request logs // Delete any issues and request logs
var issues = _issuesRepository.GetAll().Where(x => x.UserReportedId == userId); var issues = _issuesRepository.GetAll().Where(x => x.UserReportedId == userId);

@ -25,28 +25,29 @@ namespace Ombi.Core.Engine
{ {
// get all movie requests // get all movie requests
var movies = _movieRequest.GetWithUser(); var movies = _movieRequest.GetWithUser();
var filteredMovies = movies.Where(x => x.RequestedDate >= request.From && x.RequestedDate <= request.To); var filteredMovies = await movies.Where(x => x.RequestedDate >= request.From && x.RequestedDate <= request.To).ToListAsync();
var tv = _tvRequest.GetLite(); var tv = _tvRequest.GetLite();
var children = tv.SelectMany(x => var children = await tv.SelectMany(x =>
x.ChildRequests.Where(c => c.RequestedDate >= request.From && c.RequestedDate <= request.To)); x.ChildRequests.Where(c => c.RequestedDate >= request.From && c.RequestedDate <= request.To)).ToListAsync();
var userMovie = filteredMovies.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefault();
var userTv = children.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefault();
var userMovie = filteredMovies.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefaultAsync();
var userTv = children.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefaultAsync();
var moviesCount = filteredMovies.CountAsync(); var moviesCount = filteredMovies.Count;
var childrenCount = children.CountAsync(); var childrenCount = children.Count;
var availableMovies = var availableMovies =
filteredMovies.Select(x => x.MarkedAsAvailable >= request.From && x.MarkedAsAvailable <= request.To).CountAsync(); filteredMovies.Select(x => x.MarkedAsAvailable >= request.From && x.MarkedAsAvailable <= request.To).Count();
var availableChildren = children.Where(c => c.MarkedAsAvailable >= request.From && c.MarkedAsAvailable <= request.To).CountAsync(); var availableChildren = children.Where(c => c.MarkedAsAvailable >= request.From && c.MarkedAsAvailable <= request.To).Count();
return new UserStatsSummary return new UserStatsSummary
{ {
TotalMovieRequests = await moviesCount, TotalMovieRequests = moviesCount,
TotalTvRequests = await childrenCount, TotalTvRequests = childrenCount,
CompletedRequestsTv = await availableChildren, CompletedRequestsTv = availableChildren,
CompletedRequestsMovies = await availableMovies, CompletedRequestsMovies = availableMovies,
MostRequestedUserMovie = (await userMovie).FirstOrDefault()?.RequestedUser ?? new OmbiUser(), MostRequestedUserMovie = userMovie.FirstOrDefault()?.RequestedUser ?? new OmbiUser(),
MostRequestedUserTv = (await userTv).FirstOrDefault()?.RequestedUser ?? new OmbiUser(), MostRequestedUserTv = userTv.FirstOrDefault()?.RequestedUser ?? new OmbiUser(),
}; };
} }
} }

@ -1,9 +1,12 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core namespace Ombi.Core
{ {
public interface IImageService public interface IImageService
{ {
Task<string> GetTvBackground(string tvdbId); Task<string> GetTvBackground(string tvdbId);
Task<string> GetTmdbTvBackground(string id, CancellationToken token);
Task<string> GetTmdbTvPoster(string tmdbId, CancellationToken token);
} }
} }

@ -1,8 +1,13 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.FanartTv; using Ombi.Api.FanartTv;
using Ombi.Api.TheMovieDb;
using Ombi.Core.Helpers;
using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Repository; using Ombi.Store.Repository;
namespace Ombi.Core namespace Ombi.Core
@ -12,13 +17,19 @@ namespace Ombi.Core
private readonly IApplicationConfigRepository _configRepository; private readonly IApplicationConfigRepository _configRepository;
private readonly IFanartTvApi _fanartTvApi; private readonly IFanartTvApi _fanartTvApi;
private readonly ICacheService _cache; private readonly ICacheService _cache;
private readonly IMovieDbApi _movieDbApi;
private readonly ICurrentUser _user;
private readonly ISettingsService<OmbiSettings> _ombiSettings;
public ImageService(IApplicationConfigRepository configRepository, IFanartTvApi fanartTvApi, public ImageService(IApplicationConfigRepository configRepository, IFanartTvApi fanartTvApi,
ICacheService cache) ICacheService cache, IMovieDbApi movieDbApi, ICurrentUser user, ISettingsService<OmbiSettings> ombiSettings)
{ {
_configRepository = configRepository; _configRepository = configRepository;
_fanartTvApi = fanartTvApi; _fanartTvApi = fanartTvApi;
_cache = cache; _cache = cache;
_movieDbApi = movieDbApi;
_user = user;
_ombiSettings = ombiSettings;
} }
public async Task<string> GetTvBackground(string tvdbId) public async Task<string> GetTvBackground(string tvdbId)
@ -43,5 +54,69 @@ namespace Ombi.Core
return string.Empty; return string.Empty;
} }
public async Task<string> GetTmdbTvBackground(string id, CancellationToken token)
{
var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}tv{id}", () => _movieDbApi.GetTvImages(id, token), DateTimeOffset.Now.AddDays(1));
if (images?.backdrops?.Any() ?? false)
{
return images.backdrops.Select(x => x.file_path).FirstOrDefault();
}
if (images?.posters?.Any() ?? false)
{
return images.posters.Select(x => x.file_path).FirstOrDefault();
}
return string.Empty;
}
public async Task<string> GetTmdbTvPoster(string tmdbId, CancellationToken token)
{
var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}tv{tmdbId}", () => _movieDbApi.GetTvImages(tmdbId, token), DateTimeOffset.Now.AddDays(1));
if (images?.posters?.Any() ?? false)
{
var lang = await DefaultLanguageCode();
var langImage = images.posters.Where(x => lang.Equals(x.iso_639_1, StringComparison.InvariantCultureIgnoreCase)).OrderByDescending(x => x.vote_count);
if (langImage.Any())
{
return langImage.Select(x => x.file_path).First();
}
else
{
return images.posters.Select(x => x.file_path).First();
}
}
if (images?.backdrops?.Any() ?? false)
{
return images.backdrops.Select(x => x.file_path).FirstOrDefault();
}
return string.Empty;
}
protected async Task<string> DefaultLanguageCode()
{
var user = await _user.GetUser();
if (user == null)
{
return "en";
}
if (string.IsNullOrEmpty(user.Language))
{
var s = await GetOmbiSettings();
return s.DefaultLanguageCode;
}
return user.Language;
}
private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings()
{
return ombiSettings ?? (ombiSettings = await _ombiSettings.GetSettingsAsync());
}
} }
} }

@ -0,0 +1,16 @@
namespace Ombi.Core.Models
{
public class PlexUserWatchlistModel
{
public string UserId { get; set; }
public WatchlistSyncStatus SyncStatus { get; set; }
public string UserName { get; set; }
}
public enum WatchlistSyncStatus
{
Successful,
Failed,
NotEnabled
}
}

@ -0,0 +1,24 @@
using Ombi.Store.Entities;
using System;
namespace Ombi.Core.Models.Requests
{
public class RecentlyRequestedModel
{
public int RequestId { get; set; }
public RequestType Type { get; set; }
public string UserId { get; set; }
public string Username { get; set; }
public bool Available { get; set; }
public bool TvPartiallyAvailable { get; set; }
public DateTime RequestDate { get; set; }
public string Title { get; set; }
public string Overview { get; set; }
public DateTime ReleaseDate { get; set; }
public bool Approved { get; set; }
public string MediaId { get; set; }
public string PosterPath { get; set; }
public string Background { get; set; }
}
}

@ -3,6 +3,7 @@
public class TesterResultModel public class TesterResultModel
{ {
public bool IsValid { get; set; } public bool IsValid { get; set; }
public string Version { get; set; }
public string ExpectedSubDir { get; set; } public string ExpectedSubDir { get; set; }
} }
} }

@ -11,14 +11,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="10.0.0" /> <PackageReference Include="AutoMapper" Version="12.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="MusicBrainzAPI" Version="2.0.1" /> <PackageReference Include="MusicBrainzAPI" Version="2.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" /> <PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -15,6 +15,8 @@ using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using System.Collections.Generic; using System.Collections.Generic;
using Ombi.Api.Radarr.Models; using Ombi.Api.Radarr.Models;
using Microsoft.Extensions.Options;
using Ombi.Api.Sonarr;
namespace Ombi.Core.Senders namespace Ombi.Core.Senders
{ {
@ -67,7 +69,7 @@ namespace Ombi.Core.Senders
} }
if (radarrSettings.Enabled) if (radarrSettings.Enabled)
{ {
return await SendToRadarr(model, is4K, radarrSettings); return await SendToRadarr(model, radarrSettings);
} }
var dogSettings = await _dogNzbSettings.GetSettingsAsync(); var dogSettings = await _dogNzbSettings.GetSettingsAsync();
@ -131,7 +133,7 @@ namespace Ombi.Core.Senders
return await _dogNzbApi.AddMovie(settings.ApiKey, id); return await _dogNzbApi.AddMovie(settings.ApiKey, id);
} }
private async Task<SenderResult> SendToRadarr(MovieRequests model, bool is4K, RadarrSettings settings) private async Task<SenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{ {
var qualityToUse = int.Parse(settings.DefaultQualityProfile); var qualityToUse = int.Parse(settings.DefaultQualityProfile);
@ -154,6 +156,17 @@ namespace Ombi.Core.Senders
} }
} }
var tags = new List<int>();
if (settings.Tag.HasValue)
{
tags.Add(settings.Tag.Value);
}
if (settings.SendUserTags)
{
var userTag = await GetOrCreateTag(model, settings);
tags.Add(userTag.id);
}
// Overrides on the request take priority // Overrides on the request take priority
if (model.QualityOverride > 0) if (model.QualityOverride > 0)
{ {
@ -174,7 +187,7 @@ namespace Ombi.Core.Senders
{ {
var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability); settings.MinimumAvailability, tags);
if (!string.IsNullOrEmpty(result.Error?.message)) if (!string.IsNullOrEmpty(result.Error?.message))
{ {
@ -212,5 +225,17 @@ namespace Ombi.Core.Senders
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId); var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath?.path ?? string.Empty; return selectedPath?.path ?? string.Empty;
} }
private async Task<Tag> GetOrCreateTag(MovieRequests model, RadarrSettings s)
{
var tagName = model.RequestedUser.UserName;
// Does tag exist?
var allTags = await _radarrV3Api.GetTags(s.ApiKey, s.FullUri);
var existingTag = allTags.FirstOrDefault(x => x.label.Equals(tagName, StringComparison.InvariantCultureIgnoreCase));
existingTag ??= await _radarrV3Api.CreateTag(s.ApiKey, s.FullUri, tagName);
return existingTag;
}
} }
} }

@ -0,0 +1,10 @@
using Ombi.Api.Sonarr.Models;
using System.Collections.Generic;
namespace Ombi.Core.Senders
{
internal class SonarrSendOptions
{
public List<int> Tags { get; set; } = new List<int>();
}
}

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
using Ombi.Api.DogNzb; using Ombi.Api.DogNzb;
using Ombi.Api.DogNzb.Models; using Ombi.Api.DogNzb.Models;
using Ombi.Api.SickRage; using Ombi.Api.SickRage;
@ -25,8 +27,7 @@ namespace Ombi.Core.Senders
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings, ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify) ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{ {
SonarrApi = sonarrApi; SonarrApi = sonarrV3Api;
SonarrV3Api = sonarrV3Api;
Logger = log; Logger = log;
SonarrSettings = sonarrSettings; SonarrSettings = sonarrSettings;
DogNzbSettings = dog; DogNzbSettings = dog;
@ -38,8 +39,7 @@ namespace Ombi.Core.Senders
_notificationHelper = notify; _notificationHelper = notify;
} }
private ISonarrApi SonarrApi { get; } private ISonarrV3Api SonarrApi { get; }
private ISonarrV3Api SonarrV3Api { get; }
private IDogNzbApi DogNzbApi { get; } private IDogNzbApi DogNzbApi { get; }
private ISickRageApi SickRageApi { get; } private ISickRageApi SickRageApi { get; }
private ILogger<TvSender> Logger { get; } private ILogger<TvSender> Logger { get; }
@ -155,12 +155,13 @@ namespace Ombi.Core.Senders
{ {
return null; return null;
} }
var options = new SonarrSendOptions();
int qualityToUse; int qualityToUse;
var sonarrV3 = s.V3;
var languageProfileId = s.LanguageProfile; var languageProfileId = s.LanguageProfile;
string rootFolderPath; string rootFolderPath;
string seriesType; string seriesType;
int? tagToUse = null;
var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId); var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
@ -191,6 +192,7 @@ namespace Ombi.Core.Senders
} }
} }
seriesType = "anime"; seriesType = "anime";
tagToUse = s.AnimeTag;
} }
else else
{ {
@ -210,6 +212,7 @@ namespace Ombi.Core.Senders
} }
} }
seriesType = "standard"; seriesType = "standard";
tagToUse = s.Tag;
} }
// Overrides on the request take priority // Overrides on the request take priority
@ -241,6 +244,16 @@ namespace Ombi.Core.Senders
try try
{ {
if (tagToUse.HasValue)
{
options.Tags.Add(tagToUse.Value);
}
if (s.SendUserTags)
{
var userTag = await GetOrCreateTag(model, s);
options.Tags.Add(userTag.id);
}
// Does the series actually exist? // Does the series actually exist?
var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri); var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri);
var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId); var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId);
@ -265,13 +278,11 @@ namespace Ombi.Core.Senders
ignoreEpisodesWithFiles = false, // There shouldn't be any episodes with files, this is a new season ignoreEpisodesWithFiles = false, // There shouldn't be any episodes with files, this is a new season
ignoreEpisodesWithoutFiles = false, // We want all missing ignoreEpisodesWithoutFiles = false, // We want all missing
searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly. searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly.
} },
languageProfileId = languageProfileId,
tags = options.Tags
}; };
if (sonarrV3)
{
newSeries.languageProfileId = languageProfileId;
}
// Montitor the correct seasons, // Montitor the correct seasons,
// If we have that season in the model then it's monitored! // If we have that season in the model then it's monitored!
@ -283,11 +294,11 @@ namespace Ombi.Core.Senders
throw new Exception(string.Join(',', result.ErrorMessages)); throw new Exception(string.Join(',', result.ErrorMessages));
} }
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
await SendToSonarr(model, existingSeries, s); await SendToSonarr(model, existingSeries, s, options);
} }
else else
{ {
await SendToSonarr(model, existingSeries, s); await SendToSonarr(model, existingSeries, s, options);
} }
return new NewSeries return new NewSeries
@ -306,7 +317,30 @@ namespace Ombi.Core.Senders
} }
} }
private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s) private async Task<Tag> GetOrCreateTag(ChildRequests model, SonarrSettings s)
{
var tagName = model.RequestedUser.UserName;
// Does tag exist?
var allTags = await SonarrApi.GetTags(s.ApiKey, s.FullUri);
var existingTag = allTags.FirstOrDefault(x => x.label.Equals(tagName, StringComparison.InvariantCultureIgnoreCase));
existingTag ??= await SonarrApi.CreateTag(s.ApiKey, s.FullUri, tagName);
return existingTag;
}
private async Task<Tag> GetTag(int tagId, SonarrSettings s)
{
var tag = await SonarrApi.GetTag(tagId, s.ApiKey, s.FullUri);
if (tag == null)
{
Logger.LogError($"Tag ID {tagId} does not exist in sonarr. Please update the settings");
return null;
}
return tag;
}
private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s, SonarrSendOptions options)
{ {
// Check to ensure we have the all the seasons, ensure the Sonarr metadata has grabbed all the data // Check to ensure we have the all the seasons, ensure the Sonarr metadata has grabbed all the data
Season existingSeason = null; Season existingSeason = null;
@ -324,15 +358,27 @@ namespace Ombi.Core.Senders
} }
} }
var episodesToUpdate = new List<Episode>(); // Does the show have the correct tags we are expecting
// Ok, now let's sort out the episodes. if (options.Tags.Any())
{
result.tags ??= options.Tags;
var tagsToAdd = options.Tags.Except(result.tags);
if (tagsToAdd.Any())
{
result.tags.AddRange(tagsToAdd);
}
result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
}
if (model.SeriesType == SeriesType.Anime) if (model.SeriesType == SeriesType.Anime)
{ {
result.seriesType = "anime"; result.seriesType = "anime";
await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri); result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
} }
var episodesToUpdate = new List<Episode>();
// Ok, now let's sort out the episodes.
var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri); var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri);
var sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>(); var sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>();
while (!sonarrEpList.Any()) while (!sonarrEpList.Any())
@ -376,16 +422,10 @@ namespace Ombi.Core.Senders
epToUnmonitored.Add(ep); epToUnmonitored.Add(ep);
} }
foreach (var epToUpdate in epToUnmonitored) await SonarrApi.MonitorEpisode(epToUnmonitored.Select(x => x.id).ToArray(), false, s.ApiKey, s.FullUri);
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
} }
// Now update the episodes that need updating // Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber)) await SonarrApi.MonitorEpisode(episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber).Select(x => x.id).ToArray(), true, s.ApiKey, s.FullUri);
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
} }
if (!s.AddOnly) if (!s.AddOnly)
@ -527,7 +567,7 @@ namespace Ombi.Core.Senders
return rootFoldersResult.FirstOrDefault().path; return rootFoldersResult.FirstOrDefault().path;
} }
foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) foreach (var r in rootFoldersResult?.Where(r => r.id == pathId))
{ {
return r.path; return r.path;
} }

@ -0,0 +1,12 @@
using Ombi.Core.Models;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core.Services
{
public interface IPlexService
{
Task<List<PlexUserWatchlistModel>> GetWatchlistUsers(CancellationToken cancellationToken);
}
}

@ -0,0 +1,12 @@
using Ombi.Core.Models.Requests;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core.Services
{
public interface IRecentlyRequestedService
{
Task<IEnumerable<RecentlyRequestedModel>> GetRecentlyRequested(CancellationToken cancellationToken);
}
}

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UserType = Ombi.Store.Entities.UserType;
namespace Ombi.Core.Services
{
public class PlexService : IPlexService
{
private readonly IRepository<PlexWatchlistUserError> _watchlistUserErrors;
private readonly OmbiUserManager _userManager;
public PlexService(IRepository<PlexWatchlistUserError> watchlistUserErrors, OmbiUserManager userManager)
{
_watchlistUserErrors = watchlistUserErrors;
_userManager = userManager;
}
public async Task<List<PlexUserWatchlistModel>> GetWatchlistUsers(CancellationToken cancellationToken)
{
var plexUsers = _userManager.Users.Where(x => x.UserType == UserType.PlexUser);
var userErrors = await _watchlistUserErrors.GetAll().ToListAsync(cancellationToken);
var model = new List<PlexUserWatchlistModel>();
foreach(var plexUser in plexUsers)
{
model.Add(new PlexUserWatchlistModel
{
UserId = plexUser.Id,
UserName = plexUser.UserName,
SyncStatus = GetWatchlistSyncStatus(plexUser, userErrors)
});
}
return model;
}
private static WatchlistSyncStatus GetWatchlistSyncStatus(OmbiUser user, List<PlexWatchlistUserError> userErrors)
{
if (string.IsNullOrWhiteSpace(user.MediaServerToken))
{
return WatchlistSyncStatus.NotEnabled;
}
return userErrors.Any(x => x.UserId == user.Id) ? WatchlistSyncStatus.Failed : WatchlistSyncStatus.Successful;
}
}
}

@ -0,0 +1,195 @@
using Microsoft.EntityFrameworkCore;
using Ombi.Api.TheMovieDb;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Helpers;
using Ombi.Core.Models.Requests;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static Ombi.Core.Engine.BaseMediaEngine;
namespace Ombi.Core.Services
{
public class RecentlyRequestedService : BaseEngine, IRecentlyRequestedService
{
private readonly IMovieRequestRepository _movieRequestRepository;
private readonly ITvRequestRepository _tvRequestRepository;
private readonly IMusicRequestRepository _musicRequestRepository;
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly ISettingsService<OmbiSettings> _ombiSettings;
private readonly IMovieDbApi _movieDbApi;
private readonly ICacheService _cache;
private const int AmountToTake = 7;
public RecentlyRequestedService(
IMovieRequestRepository movieRequestRepository,
ITvRequestRepository tvRequestRepository,
IMusicRequestRepository musicRequestRepository,
ISettingsService<CustomizationSettings> customizationSettings,
ISettingsService<OmbiSettings> ombiSettings,
ICurrentUser user,
OmbiUserManager um,
IRuleEvaluator rules,
IMovieDbApi movieDbApi,
ICacheService cache) : base(user, um, rules)
{
_movieRequestRepository = movieRequestRepository;
_tvRequestRepository = tvRequestRepository;
_musicRequestRepository = musicRequestRepository;
_customizationSettings = customizationSettings;
_ombiSettings = ombiSettings;
_movieDbApi = movieDbApi;
_cache = cache;
}
public async Task<IEnumerable<RecentlyRequestedModel>> GetRecentlyRequested(CancellationToken cancellationToken)
{
var customizationSettingsTask = _customizationSettings.GetSettingsAsync();
var recentMovieRequests = _movieRequestRepository.GetAll().Include(x => x.RequestedUser).OrderByDescending(x => x.RequestedDate).Take(AmountToTake);
var recentTvRequests = _tvRequestRepository.GetChild().Include(x => x.RequestedUser).Include(x => x.ParentRequest).OrderByDescending(x => x.RequestedDate).Take(AmountToTake);
var recentMusicRequests = _musicRequestRepository.GetAll().Include(x => x.RequestedUser).OrderByDescending(x => x.RequestedDate).Take(AmountToTake);
var settings = await customizationSettingsTask;
if (settings.HideAvailableRecentlyRequested)
{
recentMovieRequests = recentMovieRequests.Where(x => !x.Available);
recentTvRequests = recentTvRequests.Where(x => !x.Available);
recentMusicRequests = recentMusicRequests.Where(x => !x.Available);
}
var hideUsers = await HideFromOtherUsers();
var model = new List<RecentlyRequestedModel>();
var lang = await DefaultLanguageCode();
foreach (var item in await recentMovieRequests.ToListAsync(cancellationToken))
{
if (hideUsers.Hide && item.RequestedUserId != hideUsers.UserId)
{
continue;
}
var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}movie{item.TheMovieDbId}", () => _movieDbApi.GetMovieImages(item.TheMovieDbId.ToString(), cancellationToken), DateTimeOffset.Now.AddDays(1));
model.Add(new RecentlyRequestedModel
{
RequestId = item.Id,
Available = item.Available,
Overview = item.Overview,
ReleaseDate = item.ReleaseDate,
RequestDate = item.RequestedDate,
Title = item.Title,
Type = RequestType.Movie,
Approved = item.Approved,
UserId = item.RequestedUserId,
Username = item.RequestedUser.UserAlias,
MediaId = item.TheMovieDbId.ToString(),
PosterPath = images?.posters?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(),
Background = images?.backdrops?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(),
});
}
foreach (var item in await recentMusicRequests.ToListAsync(cancellationToken))
{
if (hideUsers.Hide && item.RequestedUserId != hideUsers.UserId)
{
continue;
}
model.Add(new RecentlyRequestedModel
{
RequestId = item.Id,
Available = item.Available,
Overview = item.ArtistName,
Approved = item.Approved,
ReleaseDate = item.ReleaseDate,
RequestDate = item.RequestedDate,
Title = item.Title,
Type = RequestType.Album,
UserId = item.RequestedUserId,
Username = item.RequestedUser.UserAlias,
MediaId = item.ForeignAlbumId,
});
}
foreach (var item in await recentTvRequests.ToListAsync(cancellationToken))
{
if (hideUsers.Hide && item.RequestedUserId != hideUsers.UserId)
{
continue;
}
var providerId = item.ParentRequest.ExternalProviderId.ToString();
var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}tv{providerId}", () => _movieDbApi.GetTvImages(providerId.ToString(), cancellationToken), DateTimeOffset.Now.AddDays(1));
var partialAvailability = item.SeasonRequests.SelectMany(x => x.Episodes).Any(e => e.Available);
model.Add(new RecentlyRequestedModel
{
RequestId = item.Id,
Available = item.Available,
Overview = item.ParentRequest.Overview,
ReleaseDate = item.ParentRequest.ReleaseDate,
Approved = item.Approved,
RequestDate = item.RequestedDate,
TvPartiallyAvailable = partialAvailability,
Title = item.ParentRequest.Title,
Type = RequestType.TvShow,
UserId = item.RequestedUserId,
Username = item.RequestedUser.UserAlias,
MediaId = providerId.ToString(),
PosterPath = images?.posters?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(),
Background = images?.backdrops?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(),
});
}
return model.OrderByDescending(x => x.RequestDate);
}
private async Task<HideResult> HideFromOtherUsers()
{
var user = await GetUser();
if (await IsInRole(OmbiRoles.Admin) || await IsInRole(OmbiRoles.PowerUser) || user.IsSystemUser)
{
return new HideResult
{
UserId = user.Id
};
}
var settings = await GetOmbiSettings();
var result = new HideResult
{
Hide = settings.HideRequestsUsers,
UserId = user.Id
};
return result;
}
protected async Task<string> DefaultLanguageCode()
{
var user = await GetUser();
if (user == null)
{
return "en";
}
if (string.IsNullOrEmpty(user.Language))
{
var s = await GetOmbiSettings();
return s.DefaultLanguageCode;
}
return user.Language;
}
private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings()
{
return ombiSettings ??= await _ombiSettings.GetSettingsAsync();
}
}
}

@ -71,6 +71,8 @@ using System.Net.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Core.Services; using Ombi.Core.Services;
using Ombi.Core.Helpers; using Ombi.Core.Helpers;
using Ombi.Hubs;
using Hqub.MusicBrainz.API;
namespace Ombi.DependencyInjection namespace Ombi.DependencyInjection
{ {
@ -86,6 +88,7 @@ namespace Ombi.DependencyInjection
services.RegisterServices(); services.RegisterServices();
services.RegisterStore(); services.RegisterStore();
services.RegisterJobs(); services.RegisterJobs();
services.RegisterHubs();
} }
public static void RegisterEngines(this IServiceCollection services) public static void RegisterEngines(this IServiceCollection services)
@ -171,6 +174,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILidarrApi, LidarrApi>(); services.AddTransient<ILidarrApi, LidarrApi>();
services.AddTransient<IGroupMeApi, GroupMeApi>(); services.AddTransient<IGroupMeApi, GroupMeApi>();
services.AddTransient<IMusicBrainzApi, MusicBrainzApi>(); services.AddTransient<IMusicBrainzApi, MusicBrainzApi>();
services.AddTransient(_ => new MusicBrainzClient());
services.AddTransient<IWhatsAppApi, WhatsAppApi>(); services.AddTransient<IWhatsAppApi, WhatsAppApi>();
services.AddTransient<ICloudMobileNotification, CloudMobileNotification>(); services.AddTransient<ICloudMobileNotification, CloudMobileNotification>();
services.AddTransient<IEmbyApiFactory, EmbyApiFactory>(); services.AddTransient<IEmbyApiFactory, EmbyApiFactory>();
@ -228,8 +232,10 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILegacyMobileNotification, LegacyMobileNotification>(); services.AddTransient<ILegacyMobileNotification, LegacyMobileNotification>();
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>(); services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();
services.AddScoped<IFeatureService, FeatureService>(); services.AddScoped<IFeatureService, FeatureService>();
services.AddTransient<IRecentlyRequestedService, RecentlyRequestedService>();
services.AddTransient<IPlexService, PlexService>();
} }
public static void RegisterJobs(this IServiceCollection services) public static void RegisterJobs(this IServiceCollection services)
{ {
services.AddSingleton<QuartzJobRunner>(); services.AddSingleton<QuartzJobRunner>();
@ -266,5 +272,10 @@ namespace Ombi.DependencyInjection
services.AddTransient<IArrAvailabilityChecker, ArrAvailabilityChecker>(); services.AddTransient<IArrAvailabilityChecker, ArrAvailabilityChecker>();
services.AddTransient<IAutoDeleteRequests, AutoDeleteRequests>(); services.AddTransient<IAutoDeleteRequests, AutoDeleteRequests>();
} }
public static void RegisterHubs(this IServiceCollection services)
{
services.AddScoped<INotificationHubService, NotificationHubService>();
}
} }
} }

@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.Network" Version="3.0.1" /> <PackageReference Include="AspNetCore.HealthChecks.Network" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="System.Collections" Version="4.3.0" /> <PackageReference Include="System.Collections" Version="4.3.0" />
</ItemGroup> </ItemGroup>

@ -9,10 +9,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="nunit" Version="3.11.0" /> <PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -1,6 +1,4 @@
using Microsoft.Extensions.PlatformAbstractions; using System;
using System.Linq;
using System.Reflection;
namespace Ombi.Helpers namespace Ombi.Helpers
{ {
@ -8,9 +6,8 @@ namespace Ombi.Helpers
{ {
public static string GetRuntimeVersion() public static string GetRuntimeVersion()
{ {
ApplicationEnvironment app = PlatformServices.Default.Application; Version version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version;
var split = app.ApplicationVersion.Split('.'); return version == null ? "1.0.0" : $"{version.Major}.{version.Minor}.{version.Build}";
return string.Join('.', split.Take(3));
} }
} }
} }

@ -21,6 +21,7 @@ namespace Ombi.Helpers
public const string LidarrRootFolders = nameof(LidarrRootFolders); public const string LidarrRootFolders = nameof(LidarrRootFolders);
public const string LidarrQualityProfiles = nameof(LidarrQualityProfiles); public const string LidarrQualityProfiles = nameof(LidarrQualityProfiles);
public const string FanartTv = nameof(FanartTv); public const string FanartTv = nameof(FanartTv);
public const string TmdbImages = nameof(TmdbImages);
public const string UsersDropdown = nameof(UsersDropdown); public const string UsersDropdown = nameof(UsersDropdown);
} }
} }

@ -13,7 +13,7 @@ namespace Ombi.Helpers
} }
public class MediaCacheService : CacheService, IMediaCacheService public class MediaCacheService : CacheService, IMediaCacheService
{ {
private const string CacheKey = "MediaCacheServiceKeys"; private const string _cacheKey = "MediaCacheServiceKeys";
public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache) public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache)
{ {
@ -43,19 +43,19 @@ namespace Ombi.Helpers
private void UpdateLocalCache(string cacheKey) private void UpdateLocalCache(string cacheKey)
{ {
var mediaServiceCache = _memoryCache.Get<List<string>>(CacheKey); var mediaServiceCache = _memoryCache.Get<List<string>>(_cacheKey);
if (mediaServiceCache == null) if (mediaServiceCache == null)
{ {
mediaServiceCache = new List<string>(); mediaServiceCache = new List<string>();
} }
mediaServiceCache.Add(cacheKey); mediaServiceCache.Add(cacheKey);
_memoryCache.Remove(CacheKey); _memoryCache.Remove(_cacheKey);
_memoryCache.Set(CacheKey, mediaServiceCache); _memoryCache.Set(_cacheKey, mediaServiceCache);
} }
public Task Purge() public Task Purge()
{ {
var keys = _memoryCache.Get<List<string>>(CacheKey); var keys = _memoryCache.Get<List<string>>(_cacheKey);
if (keys == null) if (keys == null)
{ {
return Task.CompletedTask; return Task.CompletedTask;

@ -11,15 +11,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" /> <PackageReference Include="EasyCrypto" Version="4.5.0" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.1.3" /> <PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.0" /> <PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Quartz" Version="3.1.0" /> <PackageReference Include="Quartz" Version="3.5.0" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" /> <PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Hubs;
public interface INotificationHubService
{
IEnumerable<NotificationHubUser> GetOnlineUsers();
Task SendNotificationToAdmins(string data, CancellationToken token = default);
Task SendNotificationToAll(string data, CancellationToken token = default);
}

@ -1,73 +1,50 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Helpers; using Ombi.Store.Entities;
namespace Ombi.Hubs;
namespace Ombi.Hubs public class NotificationHub : Hub
{ {
public class NotificationHub : Hub private readonly OmbiUserManager _userManager;
public static readonly ConcurrentDictionary<string, NotificationHubUser> UsersOnline = new();
public NotificationHub(OmbiUserManager userManager)
{ {
public NotificationHub(OmbiUserManager um) _userManager = userManager;
{ }
_userManager = um;
}
public static ConcurrentDictionary<string, HubUsers> UsersOnline = new ConcurrentDictionary<string, HubUsers>();
public static List<string> AdminConnectionIds
{
get
{
return UsersOnline.Where(x => x.Value.Roles.Contains(OmbiRoles.Admin)).Select(x => x.Key).ToList();
}
}
public const string NotificationEvent = "Notification";
private readonly OmbiUserManager _userManager;
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{
ClaimsIdentity identity = (ClaimsIdentity)Context.User?.Identity;
Claim userIdClaim = identity?.Claims
.FirstOrDefault(x => x.Type.Equals("Id", StringComparison.InvariantCultureIgnoreCase));
if (userIdClaim == null)
{ {
var identity = (ClaimsIdentity) Context.User.Identity;
var userIdClaim = identity.Claims.FirstOrDefault(x => x.Type.Equals("Id", StringComparison.InvariantCultureIgnoreCase));
if (userIdClaim == null)
{
await base.OnConnectedAsync();
return;
}
var user = await _userManager.Users.
FirstOrDefaultAsync(x => x.Id == userIdClaim.Value);
var claims = await _userManager.GetRolesAsync(user);
UsersOnline.TryAdd(Context.ConnectionId, new HubUsers
{
UserId = userIdClaim.Value,
Roles = claims
});
await base.OnConnectedAsync(); await base.OnConnectedAsync();
return;
} }
public override Task OnDisconnectedAsync(Exception exception) OmbiUser user = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == userIdClaim.Value);
IList<string> claims = await _userManager.GetRolesAsync(user);
UsersOnline.TryAdd(Context.ConnectionId, new NotificationHubUser
{ {
UsersOnline.TryRemove(Context.ConnectionId, out _); UserId = userIdClaim.Value,
return base.OnDisconnectedAsync(exception); Roles = claims
} });
await base.OnConnectedAsync();
public Task Notification(string data)
{
return Clients.All.SendAsync(NotificationEvent, data);
}
} }
public class HubUsers public override async Task OnDisconnectedAsync(Exception exception)
{ {
public string UserId { get; set; } UsersOnline.TryRemove(Context.ConnectionId, out _);
public IList<string> Roles { get; set; } = new List<string>(); await base.OnDisconnectedAsync(exception);
} }
} }

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Ombi.Helpers;
namespace Ombi.Hubs;
public class NotificationHubService : INotificationHubService
{
public const string NotificationEvent = "Notification";
private readonly IHubContext<NotificationHub> _hubContext;
public NotificationHubService(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
public IEnumerable<NotificationHubUser> GetOnlineUsers()
{
return NotificationHub.UsersOnline.Values;
}
public Task SendNotificationToAdmins(string data, CancellationToken token = default)
{
return _hubContext.Clients
.Clients(GetConnectionIdsWithRole(OmbiRoles.Admin))
.SendAsync(NotificationEvent, data, token);
}
public Task SendNotificationToAll(string data, CancellationToken token = default)
{
return _hubContext.Clients.All.SendAsync(NotificationEvent, data, token);
}
private static List<string> GetConnectionIdsWithRole(string role)
{
return NotificationHub.UsersOnline
.Where(x => x.Value.Roles.Contains(role))
.Select(x => x.Key).ToList();
}
}

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Ombi.Hubs;
public class NotificationHubUser
{
public string UserId { get; set; }
public IList<string> Roles { get; init; } = new List<string>();
}

@ -7,8 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNet.SignalR" Version="2.4.1" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NewAlbums" xml:space="preserve">
<value>Nous Àlbums</value>
</data>
<data name="NewMovies" xml:space="preserve">
<value>Noves Pel·lícules</value>
</data>
<data name="NewTV" xml:space="preserve">
<value>Sèries Noves</value>
</data>
<data name="GenresLabel" xml:space="preserve">
<value>Gèneres:</value>
</data>
<data name="AlbumTypeLabel" xml:space="preserve">
<value>Tipus:</value>
</data>
<data name="SeasonLabel" xml:space="preserve">
<value>Temporada:</value>
</data>
<data name="EpisodesLabel" xml:space="preserve">
<value>Episodis:</value>
</data>
<data name="PoweredBy" xml:space="preserve">
<value>Desenvolupat per</value>
</data>
<data name="Unsubscribe" xml:space="preserve">
<value>Cancel·la la subscripció</value>
</data>
<data name="Album" xml:space="preserve">
<value>Àlbum</value>
</data>
<data name="Movie" xml:space="preserve">
<value>Pel·lícula</value>
</data>
<data name="TvShow" xml:space="preserve">
<value>Sèries de TV</value>
</data>
</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>New Albums</value> <value>Nový album</value>
</data> </data>
<data name="NewMovies" xml:space="preserve"> <data name="NewMovies" xml:space="preserve">
<value>New Movies</value> <value>Nové filmy</value>
</data> </data>
<data name="NewTV" xml:space="preserve"> <data name="NewTV" xml:space="preserve">
<value>New TV</value> <value>Nové seriály</value>
</data> </data>
<data name="GenresLabel" xml:space="preserve"> <data name="GenresLabel" xml:space="preserve">
<value>Žánre:</value> <value>Žánre:</value>
@ -139,18 +139,18 @@
<value>Epizódy:</value> <value>Epizódy:</value>
</data> </data>
<data name="PoweredBy" xml:space="preserve"> <data name="PoweredBy" xml:space="preserve">
<value>Powered by</value> <value>Beží na</value>
</data> </data>
<data name="Unsubscribe" xml:space="preserve"> <data name="Unsubscribe" xml:space="preserve">
<value>Unsubscribe</value> <value>Zrušiť odber</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>Seriál</value>
</data> </data>
</root> </root>

@ -11,9 +11,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="10.0.0" /> <PackageReference Include="AutoMapper" Version="12.0.0" />
<PackageReference Include="AutoMapper.Collection" Version="7.0.0" /> <PackageReference Include="AutoMapper.Collection" Version="9.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -292,6 +292,8 @@ namespace Ombi.Notifications.Tests
notificationOptions.Substitutes.Add("Season", "1"); notificationOptions.Substitutes.Add("Season", "1");
notificationOptions.Substitutes.Add("Episodes", "1, 2"); notificationOptions.Substitutes.Add("Episodes", "1, 2");
notificationOptions.Substitutes.Add("EpisodesCount", "2");
notificationOptions.Substitutes.Add("SeasonEpisodes", "1x1, 1x2");
var req = F.Build<ChildRequests>() var req = F.Build<ChildRequests>()
.With(x => x.RequestType, RequestType.TvShow) .With(x => x.RequestType, RequestType.TvShow)
.With(x => x.Available, true) .With(x => x.Available, true)
@ -324,6 +326,8 @@ namespace Ombi.Notifications.Tests
Assert.That("name", Is.EqualTo(sut.ApplicationName)); Assert.That("name", Is.EqualTo(sut.ApplicationName));
Assert.That(sut.PartiallyAvailableEpisodeNumbers, Is.EqualTo("1, 2")); Assert.That(sut.PartiallyAvailableEpisodeNumbers, Is.EqualTo("1, 2"));
Assert.That(sut.PartiallyAvailableSeasonNumber, Is.EqualTo("1")); Assert.That(sut.PartiallyAvailableSeasonNumber, Is.EqualTo("1"));
Assert.That(sut.PartiallyAvailableEpisodeCount, Is.EqualTo("2"));
Assert.That(sut.PartiallyAvailableEpisodesList, Is.EqualTo("1x1, 1x2"));
} }
[Test] [Test]

@ -6,14 +6,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" /> <PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="Nunit" Version="3.11.0" /> <PackageReference Include="Nunit" Version="3.13.3" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.15.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.8.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"></packagereference>
<PackageReference Include="Moq" Version="4.10.0" /> <PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Moq.AutoMock" Version="0.4.0" /> <PackageReference Include="Moq.AutoMock" Version="3.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -107,7 +107,7 @@ namespace Ombi.Notifications.Agents
var discordBody = new DiscordWebhookBody var discordBody = new DiscordWebhookBody
{ {
content = model.Message, content = model.Message,
username = settings.Username, username = settings.Username ?? "Ombi",
}; };
var fields = new List<DiscordField>(); var fields = new List<DiscordField>();

@ -63,7 +63,7 @@ namespace Ombi.Notifications.Agents
// Get admin devices // Get admin devices
var playerIds = await GetPrivilegedUsersPlayerIds(); var playerIds = await GetPrivilegedUsersPlayerIds();
await Send(playerIds, notification, settings, model, true); await Send(playerIds, notification);
} }
protected override async Task NewIssue(NotificationOptions model, MobileNotificationSettings settings) protected override async Task NewIssue(NotificationOptions model, MobileNotificationSettings settings)
@ -83,7 +83,7 @@ namespace Ombi.Notifications.Agents
// Get admin devices // Get admin devices
var playerIds = await GetAdmins(); var playerIds = await GetAdmins();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task IssueComment(NotificationOptions model, MobileNotificationSettings settings) protected override async Task IssueComment(NotificationOptions model, MobileNotificationSettings settings)
@ -107,13 +107,13 @@ namespace Ombi.Notifications.Agents
model.Substitutes.TryGetValue("IssueId", out var issueId); model.Substitutes.TryGetValue("IssueId", out var issueId);
// Send to user // Send to user
var playerIds = await GetUsersForIssue(model, int.Parse(issueId), NotificationType.IssueComment); var playerIds = await GetUsersForIssue(model, int.Parse(issueId), NotificationType.IssueComment);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
else else
{ {
// Send to admin // Send to admin
var playerIds = await GetAdmins(); var playerIds = await GetAdmins();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
} }
} }
@ -136,7 +136,7 @@ namespace Ombi.Notifications.Agents
// Send to user // Send to user
var playerIds = await GetUsers(model, NotificationType.IssueResolved); var playerIds = await GetUsers(model, NotificationType.IssueResolved);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
@ -158,7 +158,7 @@ namespace Ombi.Notifications.Agents
// Get admin devices // Get admin devices
var playerIds = await GetAdmins(); var playerIds = await GetAdmins();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task RequestDeclined(NotificationOptions model, MobileNotificationSettings settings) protected override async Task RequestDeclined(NotificationOptions model, MobileNotificationSettings settings)
@ -179,7 +179,7 @@ namespace Ombi.Notifications.Agents
// Send to user // Send to user
var playerIds = await GetUsers(model, NotificationType.RequestDeclined); var playerIds = await GetUsers(model, NotificationType.RequestDeclined);
await AddSubscribedUsers(playerIds); await AddSubscribedUsers(playerIds);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task RequestApproved(NotificationOptions model, MobileNotificationSettings settings) protected override async Task RequestApproved(NotificationOptions model, MobileNotificationSettings settings)
@ -201,7 +201,7 @@ namespace Ombi.Notifications.Agents
var playerIds = await GetUsers(model, NotificationType.RequestApproved); var playerIds = await GetUsers(model, NotificationType.RequestApproved);
await AddSubscribedUsers(playerIds); await AddSubscribedUsers(playerIds);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task AvailableRequest(NotificationOptions model, MobileNotificationSettings settings) protected override async Task AvailableRequest(NotificationOptions model, MobileNotificationSettings settings)
@ -225,7 +225,7 @@ namespace Ombi.Notifications.Agents
var playerIds = await GetUsers(model, NotificationType.RequestAvailable); var playerIds = await GetUsers(model, NotificationType.RequestAvailable);
await AddSubscribedUsers(playerIds); await AddSubscribedUsers(playerIds);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
private static Dictionary<string,string> GetNotificationData(NotificationMessageContent parsed, NotificationType type) private static Dictionary<string,string> GetNotificationData(NotificationMessageContent parsed, NotificationType type)
@ -240,7 +240,7 @@ namespace Ombi.Notifications.Agents
throw new NotImplementedException(); throw new NotImplementedException();
} }
protected async Task Send(List<string> playerIds, NotificationMessage model, MobileNotificationSettings settings, NotificationOptions requestModel, bool isAdminNotification = false) protected async Task Send(List<string> playerIds, NotificationMessage model)
{ {
if (playerIds == null || !playerIds.Any()) if (playerIds == null || !playerIds.Any())
{ {
@ -276,7 +276,7 @@ namespace Ombi.Notifications.Agents
} }
var playerIds = user.NotificationUserIds.Select(x => x.PlayerId).ToList(); var playerIds = user.NotificationUserIds.Select(x => x.PlayerId).ToList();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
private async Task<List<string>> GetAdmins() private async Task<List<string>> GetAdmins()
@ -382,13 +382,15 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "New Request", Subject = "Request Partially Available",
Data = GetNotificationData(parsed, NotificationType.PartiallyAvailable) Data = GetNotificationData(parsed, NotificationType.PartiallyAvailable)
}; };
// Get admin devices
var playerIds = await GetAdmins(); var playerIds = await GetUsers(model, NotificationType.PartiallyAvailable);
await Send(playerIds, notification, settings, model, true);
await AddSubscribedUsers(playerIds);
await Send(playerIds, notification);
} }
} }
} }

@ -186,6 +186,14 @@ namespace Ombi.Notifications
{ {
PartiallyAvailableEpisodeNumbers = epNumber; PartiallyAvailableEpisodeNumbers = epNumber;
} }
if (opts.Substitutes.TryGetValue("EpisodesCount", out var epCount))
{
PartiallyAvailableEpisodeCount = epCount;
}
if (opts.Substitutes.TryGetValue("SeasonEpisodes", out var sEpisodes))
{
PartiallyAvailableEpisodesList = sEpisodes;
}
} }
} }
@ -295,6 +303,8 @@ namespace Ombi.Notifications
public string ProviderId { get; set; } public string ProviderId { get; set; }
public string PartiallyAvailableEpisodeNumbers { get; set; } public string PartiallyAvailableEpisodeNumbers { get; set; }
public string PartiallyAvailableSeasonNumber { get; set; } public string PartiallyAvailableSeasonNumber { get; set; }
public string PartiallyAvailableEpisodeCount { get; set; }
public string PartiallyAvailableEpisodesList { get; set; }
// System Defined // System Defined
private string LongDate => DateTime.Now.ToString("D"); private string LongDate => DateTime.Now.ToString("D");
@ -336,6 +346,8 @@ namespace Ombi.Notifications
{ nameof(ProviderId), ProviderId }, { nameof(ProviderId), ProviderId },
{ nameof(PartiallyAvailableEpisodeNumbers), PartiallyAvailableEpisodeNumbers }, { nameof(PartiallyAvailableEpisodeNumbers), PartiallyAvailableEpisodeNumbers },
{ nameof(PartiallyAvailableSeasonNumber), PartiallyAvailableSeasonNumber }, { nameof(PartiallyAvailableSeasonNumber), PartiallyAvailableSeasonNumber },
{ nameof(PartiallyAvailableEpisodesList), PartiallyAvailableEpisodesList },
{ nameof(PartiallyAvailableEpisodeCount), PartiallyAvailableEpisodeCount },
}; };
} }
} }

@ -11,8 +11,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Ensure.That" Version="7.0.0-pre32" /> <PackageReference Include="Ensure.That" Version="10.1.0" />
<PackageReference Include="MailKit" Version="2.5.0" /> <PackageReference Include="MailKit" Version="3.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -0,0 +1,191 @@
using Microsoft.Extensions.Logging;
using MockQueryable.Moq;
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core;
using Ombi.Hubs;
using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using Ombi.Tests;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ombi.Schedule.Tests
{
[TestFixture]
public class AvailabilityCheckerTests
{
private AutoMocker _mocker;
private TestAvailabilityChecker _subject;
[SetUp]
public void SetUp()
{
_mocker = new AutoMocker();
var hub = SignalRHelper.MockHub<NotificationHub>();
_mocker.Use(hub);
_subject = _mocker.CreateInstance<TestAvailabilityChecker>();
}
[Test]
public async Task All_Episodes_Are_Available_In_Request()
{
var request = new ChildRequests
{
Title = "Test",
Id = 1,
RequestedUser = new OmbiUser { Email = "" },
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Available = false,
EpisodeNumber = 1,
Season = new SeasonRequests
{
SeasonNumber = 1
}
},
new EpisodeRequests
{
Available = false,
EpisodeNumber = 2,
Season = new SeasonRequests
{
SeasonNumber = 1
}
}
}
}
}
};
var databaseEpisodes = new List<IBaseMediaServerEpisode>
{
new PlexEpisode
{
EpisodeNumber = 1,
SeasonNumber = 1,
},
new PlexEpisode
{
EpisodeNumber = 2,
SeasonNumber = 1,
},
}.AsQueryable().BuildMock();
await _subject.ProcessTvShow(databaseEpisodes, request);
Assert.Multiple(() =>
{
Assert.That(request.Available, Is.True);
Assert.That(request.MarkedAsAvailable, Is.Not.Null);
Assert.That(request.SeasonRequests[0].Episodes[0].Available, Is.True);
Assert.That(request.SeasonRequests[0].Episodes[1].Available, Is.True);
});
Assert.Multiple(() =>
{
_mocker.Verify<ITvRequestRepository>(x => x.Save(), Times.Exactly(2));
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == Helpers.NotificationType.RequestAvailable && x.RequestId == 1)), Times.Once);
});
}
[Test]
public async Task All_One_Episode_Is_Available_In_Request()
{
var request = new ChildRequests
{
Title = "Test",
Id = 1,
RequestedUser = new OmbiUser { Email = "" },
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Available = false,
EpisodeNumber = 1,
Season = new SeasonRequests
{
SeasonNumber = 1
}
},
new EpisodeRequests
{
Available = false,
EpisodeNumber = 2,
Season = new SeasonRequests
{
SeasonNumber = 1
}
},
new EpisodeRequests
{
Available = true,
EpisodeNumber = 3,
Season = new SeasonRequests
{
SeasonNumber = 1
}
}
}
}
}
};
var databaseEpisodes = new List<IBaseMediaServerEpisode>
{
new PlexEpisode
{
EpisodeNumber = 1,
SeasonNumber = 1,
},
new PlexEpisode
{
EpisodeNumber = 3,
SeasonNumber = 1,
},
}.AsQueryable().BuildMock();
await _subject.ProcessTvShow(databaseEpisodes, request);
Assert.Multiple(() =>
{
Assert.That(request.Available, Is.False);
Assert.That(request.MarkedAsAvailable, Is.Null);
Assert.That(request.SeasonRequests[0].Episodes[0].Available, Is.True);
Assert.That(request.SeasonRequests[0].Episodes[1].Available, Is.False);
});
Assert.Multiple(() =>
{
_mocker.Verify<ITvRequestRepository>(x => x.Save(), Times.Once);
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == Helpers.NotificationType.PartiallyAvailable && x.RequestId == 1)), Times.Once);
});
}
}
public class TestAvailabilityChecker : AvailabilityChecker
{
public TestAvailabilityChecker(ITvRequestRepository tvRequest, INotificationHelper notification, ILogger log, INotificationHubService notificationHubService) : base(tvRequest, notification, log, notificationHubService)
{
}
public new Task ProcessTvShow(IQueryable<IBaseMediaServerEpisode> seriesEpisodes, ChildRequests child) => base.ProcessTvShow(seriesEpisodes, child);
}
}

@ -9,7 +9,6 @@ using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using System.Threading.Tasks; using System.Threading.Tasks;
using MockQueryable;
using MockQueryable.Moq; using MockQueryable.Moq;
namespace Ombi.Schedule.Tests namespace Ombi.Schedule.Tests
@ -51,7 +50,7 @@ namespace Ombi.Schedule.Tests
}; };
Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 }); Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 });
Repo.Setup(x => x.GetAll()).Returns(new List<Issues>(issues).AsQueryable().BuildMock().Object); Repo.Setup(x => x.GetAll()).Returns(new List<Issues>(issues).AsQueryable().BuildMock());
await Job.Execute(null); await Job.Execute(null);
Assert.That(issues.First().Status, Is.EqualTo(IssueStatus.Deleted)); Assert.That(issues.First().Status, Is.EqualTo(IssueStatus.Deleted));
@ -76,7 +75,7 @@ namespace Ombi.Schedule.Tests
}; };
Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 }); Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 });
Repo.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Issues>(issues).AsQueryable().BuildMock().Object); Repo.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Issues>(issues).AsQueryable().BuildMock());
await Job.Execute(null); await Job.Execute(null);
Assert.That(issues[0].Status, Is.Not.EqualTo(IssueStatus.Deleted)); Assert.That(issues[0].Status, Is.Not.EqualTo(IssueStatus.Deleted));
@ -102,7 +101,7 @@ namespace Ombi.Schedule.Tests
}; };
Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 }); Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 });
Repo.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Issues>(issues).AsQueryable().BuildMock().Object); Repo.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Issues>(issues).AsQueryable().BuildMock());
await Job.Execute(null); await Job.Execute(null);
Assert.That(issues[0].Status, Is.Not.EqualTo(IssueStatus.Deleted)); Assert.That(issues[0].Status, Is.Not.EqualTo(IssueStatus.Deleted));

@ -7,19 +7,20 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="MockQueryable.Moq" Version="5.0.0-preview.7" /> <PackageReference Include="MockQueryable.Moq" Version="6.0.1" />
<PackageReference Include="Moq" Version="4.15.1" /> <PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Moq.AutoMock" Version="0.4.0" /> <PackageReference Include="Moq.AutoMock" Version="3.4.0" />
<PackageReference Include="Nunit" Version="3.11.0" /> <PackageReference Include="Nunit" Version="3.13.3" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.15.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"></packagereference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" /> <ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" />
<ProjectReference Include="..\Ombi.Test.Common\Ombi.Test.Common.csproj" /> <ProjectReference Include="..\Ombi.Test.Common\Ombi.Test.Common.csproj" />
<ProjectReference Include="..\Ombi.Tests\Ombi.Tests.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -1,16 +1,11 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Castle.Components.DictionaryAdapter; using Castle.Components.DictionaryAdapter;
using Microsoft.AspNetCore.SignalR;
using Moq; using Moq;
using MockQueryable.Moq; using MockQueryable.Moq;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core; using Ombi.Core;
using Ombi.Core.Notifications;
using Ombi.Hubs; using Ombi.Hubs;
using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Plex;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -19,49 +14,140 @@ using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests; using Ombi.Store.Repository.Requests;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Core.Services; using Ombi.Core.Services;
using Ombi.Tests;
using Moq.AutoMock;
using Ombi.Settings.Settings.Models;
using Ombi.Notifications.Models;
namespace Ombi.Schedule.Tests namespace Ombi.Schedule.Tests
{ {
[TestFixture] [TestFixture]
[Ignore("Need to work out how to mockout the hub context")]
public class PlexAvailabilityCheckerTests public class PlexAvailabilityCheckerTests
{ {
private AutoMocker _mocker;
private PlexAvailabilityChecker _subject;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_repo = new Mock<IPlexContentRepository>(); _mocker = new AutoMocker();
_tv = new Mock<ITvRequestRepository>();
_movie = new Mock<IMovieRequestRepository>(); var hub = SignalRHelper.MockHub<NotificationHub>();
_notify = new Mock<INotificationHelper>(); _mocker.Use(hub);
var hub = new Mock<IHubContext<NotificationHub>>();
hub.Setup(x => _subject = _mocker.CreateInstance<PlexAvailabilityChecker>();
x.Clients.Clients(It.IsAny<IReadOnlyList<string>>()).SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>())); }
NotificationHub.UsersOnline.TryAdd("A", new HubUsers());
Checker = new PlexAvailabilityChecker(_repo.Object, _tv.Object, _movie.Object, _notify.Object, null, hub.Object, Mock.Of<IFeatureService>()); [Test]
public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex_WithImdbId()
{
var request = new MovieRequests
{
ImdbId = "test"
};
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent());
await _subject.Execute(null);
Assert.Multiple(() =>
{
Assert.That(request.Available, Is.True);
Assert.That(request.MarkedAsAvailable, Is.Not.Null);
Assert.That(request.Available4K, Is.False);
Assert.That(request.MarkedAsAvailable4K, Is.Null);
});
_mocker.Verify<IMovieRequestRepository>(x => x.SaveChangesAsync(), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get("test", ProviderType.ImdbId), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.TheMovieDbId), Times.Never);
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == NotificationType.RequestAvailable)), Times.Once);
} }
[Test]
public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex_WithTheMovieDbId()
{
var request = new MovieRequests
{
ImdbId = null,
TheMovieDbId = 33
};
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x => x.Get(It.IsAny<string>(), ProviderType.ImdbId)).ReturnsAsync((PlexServerContent)null);
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x => x.Get("33", ProviderType.TheMovieDbId)).ReturnsAsync(new PlexServerContent());
await _subject.Execute(null);
Assert.Multiple(() =>
{
Assert.That(request.Available, Is.True);
Assert.That(request.MarkedAsAvailable, Is.Not.Null);
Assert.That(request.Available4K, Is.False);
Assert.That(request.MarkedAsAvailable4K, Is.Null);
});
private Mock<IPlexContentRepository> _repo; _mocker.Verify<IMovieRequestRepository>(x => x.SaveChangesAsync(), Times.Once);
private Mock<ITvRequestRepository> _tv; _mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.ImdbId), Times.Never);
private Mock<IMovieRequestRepository> _movie; _mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.TheMovieDbId), Times.Once);
private Mock<INotificationHelper> _notify; _mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == NotificationType.RequestAvailable)), Times.Once);
private PlexAvailabilityChecker Checker; }
[Test] [Test]
public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex() public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex_WithTheMovieDbId_4K_Enabled ()
{ {
_mocker.Setup<IFeatureService, Task<bool>>(x => x.FeatureEnabled(FeatureNames.Movie4KRequests)).ReturnsAsync(true);
var request = new MovieRequests var request = new MovieRequests
{ {
ImdbId = "test" ImdbId = "test"
}; };
_movie.Setup(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable()); _mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
_repo.Setup(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent()); _mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent { Quality = "1080p" });
await Checker.Execute(null); await _subject.Execute(null);
_movie.Verify(x => x.Save(), Times.Once); Assert.Multiple(() =>
{
Assert.That(request.Available, Is.True);
Assert.That(request.MarkedAsAvailable, Is.Not.Null);
Assert.That(request.Available4K, Is.False);
Assert.That(request.MarkedAsAvailable4K, Is.Null);
});
Assert.True(request.Available); _mocker.Verify<IMovieRequestRepository>(x => x.SaveChangesAsync(), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get("test", ProviderType.ImdbId), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.TheMovieDbId), Times.Never);
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == NotificationType.RequestAvailable)), Times.Once);
}
[Test]
public async Task ProcessMovies_4K_ShouldMarkAvailable_WhenInPlex_WithImdbId_And_4K_FeatureEnabled()
{
_mocker.Setup<IFeatureService, Task<bool>>(x => x.FeatureEnabled(FeatureNames.Movie4KRequests)).ReturnsAsync(true);
var request = new MovieRequests
{
ImdbId = "test",
Is4kRequest = true,
Has4KRequest = true,
};
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent { Has4K = true });
await _subject.Execute(null);
_mocker.Verify<IMovieRequestRepository>(x => x.SaveChangesAsync(), Times.Once);
Assert.Multiple(() =>
{
Assert.That(request.Available, Is.False);
Assert.That(request.MarkedAsAvailable, Is.Null);
Assert.That(request.Available4K, Is.True);
Assert.That(request.MarkedAsAvailable4K, Is.Not.Null);
});
_mocker.Verify<IMovieRequestRepository>(x => x.SaveChangesAsync(), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get("test", ProviderType.ImdbId), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.TheMovieDbId), Times.Never);
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == NotificationType.RequestAvailable)), Times.Once);
} }
[Test] [Test]
@ -71,19 +157,96 @@ namespace Ombi.Schedule.Tests
{ {
ImdbId = "test" ImdbId = "test"
}; };
_movie.Setup(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable()); _mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
await Checker.Execute(null); await _subject.Execute(null);
Assert.False(request.Available); Assert.False(request.Available);
} }
[Test] [Test]
public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex() public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex_MovieDbId()
{
var request = CreateChildRequest(null, 33, 99);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock());
_mocker.Setup<IPlexContentRepository, IQueryable<IMediaServerEpisode>>(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
{
new PlexEpisode
{
Series = new PlexServerContent
{
TheMovieDbId = 33.ToString(),
Title = "abc"
},
EpisodeNumber = 1,
SeasonNumber = 2,
}
}.AsQueryable().BuildMock());
await _subject.Execute(null);
_mocker.Verify<ITvRequestRepository>(x => x.Save(), Times.AtLeastOnce);
Assert.True(request.SeasonRequests[0].Episodes[0].Available);
}
[Test]
public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex_ImdbId()
{
var request = CreateChildRequest("abc", -1, 99);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock());
_mocker.Setup<IPlexContentRepository, IQueryable<IMediaServerEpisode>>(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
{
new PlexEpisode
{
Series = new PlexServerContent
{
ImdbId = "abc",
},
EpisodeNumber = 1,
SeasonNumber = 2,
}
}.AsQueryable().BuildMock());
await _subject.Execute(null);
_mocker.Verify<ITvRequestRepository>(x => x.Save(), Times.AtLeastOnce);
Assert.True(request.SeasonRequests[0].Episodes[0].Available);
}
[Test]
public async Task ProcessTv_ShouldMark_Episode_Available_By_TitleMatch()
{ {
var request = new ChildRequests var request = CreateChildRequest("abc", -1, 99);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock());
_mocker.Setup<IPlexContentRepository, IQueryable<IMediaServerEpisode>>(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
{ {
ParentRequest = new TvRequests { TvDbId = 1 }, new PlexEpisode
{
Series = new PlexServerContent
{
Title = "UnitTest",
ImdbId = "invlaid",
},
EpisodeNumber = 1,
SeasonNumber = 2,
}
}.AsQueryable().BuildMock());
await _subject.Execute(null);
_mocker.Verify<ITvRequestRepository>(x => x.Save(), Times.AtLeastOnce);
Assert.True(request.SeasonRequests[0].Episodes[0].Available);
}
private ChildRequests CreateChildRequest(string imdbId, int theMovieDbId, int tvdbId)
{
return new ChildRequests
{
Title = "UnitTest",
ParentRequest = new TvRequests { ImdbId = imdbId, ExternalProviderId = theMovieDbId, TvDbId = tvdbId },
SeasonRequests = new EditableList<SeasonRequests> SeasonRequests = new EditableList<SeasonRequests>
{ {
new SeasonRequests new SeasonRequests
@ -93,7 +256,7 @@ namespace Ombi.Schedule.Tests
new EpisodeRequests new EpisodeRequests
{ {
EpisodeNumber = 1, EpisodeNumber = 1,
Season = new SeasonRequests Season = new SeasonRequests
{ {
SeasonNumber = 2 SeasonNumber = 2
} }
@ -106,27 +269,6 @@ namespace Ombi.Schedule.Tests
Email = "abc" Email = "abc"
} }
}; };
_tv.Setup(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock().Object);
_repo.Setup(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
{
new PlexEpisode
{
Series = new PlexServerContent
{
TvDbId = 1.ToString(),
},
EpisodeNumber = 1,
SeasonNumber = 2
}
}.AsQueryable().BuildMock().Object);
_repo.Setup(x => x.Include(It.IsAny<IQueryable<PlexEpisode>>(),It.IsAny<Expression<Func<PlexEpisode, PlexServerContent>>>()));
await Checker.Execute(null);
_tv.Verify(x => x.Save(), Times.Once);
Assert.True(request.SeasonRequests[0].Episodes[0].Available);
} }
} }
} }

@ -2,9 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Moq.AutoMock; using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
@ -48,7 +46,7 @@ namespace Ombi.Schedule.Tests
}; };
var contentToAdd = new HashSet<PlexServerContent>(); var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>(); var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexContentRepository>(x => _mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x =>
x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>())) x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>()))
.Returns(Task.FromResult(new PlexServerContent())); .Returns(Task.FromResult(new PlexServerContent()));
@ -108,7 +106,7 @@ namespace Ombi.Schedule.Tests
}; };
var contentToAdd = new HashSet<PlexServerContent>(); var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>(); var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexApi>(x => x.GetMetadata(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())) _mocker.Setup<IPlexApi, Task<PlexMetadata>>(x => x.GetMetadata(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(new PlexMetadata .Returns(Task.FromResult(new PlexMetadata
{ {
MediaContainer = new Mediacontainer MediaContainer = new Mediacontainer
@ -166,7 +164,7 @@ namespace Ombi.Schedule.Tests
}; };
var contentToAdd = new HashSet<PlexServerContent>(); var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>(); var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexContentRepository>(x => _mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x =>
x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>())) x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>()))
.Returns(Task.FromResult(new PlexServerContent .Returns(Task.FromResult(new PlexServerContent
{ {
@ -204,7 +202,7 @@ namespace Ombi.Schedule.Tests
}; };
var contentToAdd = new HashSet<PlexServerContent>(); var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>(); var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexContentRepository>(x => _mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x =>
x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>())) x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>()))
.Returns(Task.FromResult(new PlexServerContent .Returns(Task.FromResult(new PlexServerContent
{ {

@ -0,0 +1,407 @@
using Microsoft.AspNetCore.Identity;
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Test.Common;
using Ombi.Tests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ombi.Schedule.Tests
{
[TestFixture]
public class PlexUserImporterTests
{
private List<OmbiUser> _users = new List<OmbiUser>
{
new OmbiUser { Id = Guid.NewGuid().ToString("N"), UserName="abc", NormalizedUserName = "ABC", UserType = UserType.LocalUser},
new OmbiUser { Id = Guid.NewGuid().ToString("N"), UserName="sys", NormalizedUserName = "SYS", UserType = UserType.SystemUser},
new OmbiUser { Id = Guid.NewGuid().ToString("N"), UserName="plex", NormalizedUserName = "PLEX", UserType = UserType.PlexUser, ProviderUserId = "PLEX_ID", Email = "dupe"},
};
private AutoMocker _mocker;
private PlexUserImporter _subject;
[SetUp]
public void SetUp()
{
_mocker = new AutoMocker();
var um = MockHelper.MockUserManager(_users);
var hub = SignalRHelper.MockHub<NotificationHub>();
_mocker.Use(um);
_mocker.Use(hub);
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings
{
Enable = true,
Servers = new List<PlexServers>
{
new PlexServers { Name = "Test", MachineIdentifier = "123", PlexAuthToken = "abc" }
}
});
_subject = _mocker.CreateInstance<PlexUserImporter>();
}
[Test]
public async Task Import_Exits_WhenNot_Enabled()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = false });
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Exits_When_Plex_Not_Enabled()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = true, ImportPlexUsers = true });
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = false });
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Exits_When_Plex_No_AuthToken()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = true, ImportPlexUsers = true });
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings
{
Enable = true,
Servers = new List<PlexServers>
{
new PlexServers { Name = "Test", MachineIdentifier = "123", PlexAuthToken = null }
}
});
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Only_Imports_Plex_Admin()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = true, ImportPlexUsers = false });
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "email",
authentication_token = "user_token",
title = "user_title",
username = "user_username",
id = "user_id",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "user_username" && x.Email == "email" && x.ProviderUserId == "user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "user_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Once);
}
[Test]
public async Task Import_Only_Imports_Plex_Admin_Already_Exists()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = true, ImportPlexUsers = false });
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "email",
authentication_token = "user_token",
title = "user_title",
username = "newUsername",
id = "PLEX_ID",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "user_username" && x.Email == "email" && x.ProviderUserId == "user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "user_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
_mocker.Verify<OmbiUserManager>(x => x.UpdateAsync(It.Is<OmbiUser>(x => x.Email == "email" && x.UserName == "newUsername")), Times.Once);
}
[Test]
public async Task Import_Only_Imports_Plex_Admin_Username_Clash()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = true, ImportPlexUsers = false });
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "email",
authentication_token = "user_token",
title = "user_title",
username = "abc",
id = "nah",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "user_username" && x.Email == "email" && x.ProviderUserId == "user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "user_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
_mocker.Verify<OmbiUserManager>(x => x.UpdateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Doesnt_Import_Banned_Users()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true, BannedPlexUserIds = new List<string> { "Banned" } });
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
new UserFriends
{
Email = "email",
Id = "Banned",
Title = "title",
Username = "username"
}
}
});
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Doesnt_Import_Managed_User()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true });
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
new UserFriends
{
Email = "email",
Id = "id",
Title = "title",
}
}
});
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Doesnt_Import_DuplicateEmail()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true });
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
new UserFriends
{
Email = "dupe",
Id = "id",
Title = "title",
Username = "username"
}
}
});
_mocker.Setup<OmbiUserManager, Task<OmbiUser>>(x => x.FindByEmailAsync("dupe")).ReturnsAsync(new OmbiUser());
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Never);
}
[Test]
public async Task Import_Created_Plex_User()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings { ImportPlexAdmin = false, ImportPlexUsers = true, DefaultRoles = new List<string>
{
OmbiRoles.RequestMovie
}
});
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
new UserFriends
{
Email = "email",
Id = "id",
Username = "plex"
}
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "plex" && x.Email == "email" && x.ProviderUserId == "id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "plex"), OmbiRoles.RequestMovie))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.CreateAsync(It.IsAny<OmbiUser>()), Times.Once);
}
[Test]
public async Task Import_Update_Plex_User()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings
{
ImportPlexAdmin = false,
ImportPlexUsers = true,
DefaultRoles = new List<string>
{
OmbiRoles.RequestMovie
}
});
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
new UserFriends
{
Email = "email",
Id = "PLEX_ID",
Username = "user"
}
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "plex" && x.Email == "email" && x.ProviderUserId == "id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "plex"), OmbiRoles.RequestMovie))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.UpdateAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "email" && x.UserName == "user")), Times.Once);
}
[Test]
public async Task Import_Cleanup_Missing_Plex_Users()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings
{
ImportPlexAdmin = true,
ImportPlexUsers = true,
DefaultRoles = new List<string>
{
OmbiRoles.RequestMovie
},
CleanupPlexUsers = true,
});
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
}
});
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "email",
authentication_token = "user_token",
title = "user_title",
username = "user_username",
id = "user_id",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "user_username" && x.Email == "email" && x.ProviderUserId == "user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "user_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.DeleteAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "dupe" && x.UserName == "plex")), Times.Once);
}
[Test]
public async Task Import_Cleanup_Missing_Plex_Admin()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings
{
ImportPlexAdmin = true,
ImportPlexUsers = false,
DefaultRoles = new List<string>
{
OmbiRoles.RequestMovie
},
CleanupPlexUsers = true,
});
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "diff_email",
authentication_token = "user_token",
title = "user_title",
username = "diff_username",
id = "diff_user_id",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "diff_username" && x.Email == "diff_email" && x.ProviderUserId == "diff_user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "diff_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.DeleteAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "dupe" && x.UserName == "plex")), Times.Once);
}
}
}

@ -1,8 +1,11 @@
using Moq; using MockQueryable.Moq;
using Moq;
using Moq.AutoMock; using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Engine; using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
@ -32,11 +35,12 @@ namespace Ombi.Schedule.Tests
public void Setup() public void Setup()
{ {
_mocker = new AutoMocker(); _mocker = new AutoMocker();
var um = MockHelper.MockUserManager(new List<OmbiUser> { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "abc", UserName = "abc", NormalizedUserName = "ABC" } }); var um = MockHelper.MockUserManager(new List<OmbiUser> { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "token1", UserName = "abc", NormalizedUserName = "ABC" } });
_mocker.Use(um); _mocker.Use(um);
_context = _mocker.GetMock<IJobExecutionContext>(); _context = _mocker.GetMock<IJobExecutionContext>();
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); _context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
_subject = _mocker.CreateInstance<PlexWatchlistImport>(); _subject = _mocker.CreateInstance<PlexWatchlistImport>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
} }
[Test] [Test]
@ -53,6 +57,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task TerminatesWhenWatchlistIsNotEnabled() public async Task TerminatesWhenWatchlistIsNotEnabled()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false });
await _subject.Execute(null); await _subject.Execute(null);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never); _mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
@ -75,9 +80,74 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
} }
[Test]
public async Task AuthenticationError()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer { AuthError = true });
await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
_mocker.Verify<IRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Add(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Once);
}
[Test]
public async Task FailedWatchListUser_NewToken_ShouldBeRemoved()
{
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>
{
new PlexWatchlistUserError
{
UserId = "abc",
MediaServerToken = "dead"
}
}.AsQueryable().BuildMock());
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer { AuthError = false });
await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Add(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Never);
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Delete(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Once);
}
[Test]
public async Task FailedWatchListUser_OldToken_ShouldSkip()
{
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>
{
new PlexWatchlistUserError
{
UserId = "abc",
MediaServerToken = "token1"
}
}.AsQueryable().BuildMock());
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer { AuthError = false });
await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Add(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Never);
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Delete(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Never);
}
[Test] [Test]
public async Task NoPlexUsersWithToken() public async Task NoPlexUsersWithToken()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
var um = MockHelper.MockUserManager(new List<OmbiUser> var um = MockHelper.MockUserManager(new List<OmbiUser>
{ {
@ -102,6 +172,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MultipleUsers() public async Task MultipleUsers()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
var um = MockHelper.MockUserManager(new List<OmbiUser> var um = MockHelper.MockUserManager(new List<OmbiUser>
{ {
@ -125,6 +196,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_NoGuid() public async Task MovieRequestFromWatchList_NoGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -175,6 +247,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task TvRequestFromWatchList_NoGuid() public async Task TvRequestFromWatchList_NoGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -224,6 +297,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_AlreadyRequested() public async Task MovieRequestFromWatchList_AlreadyRequested()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -322,6 +396,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_NoTmdbGuid() public async Task MovieRequestFromWatchList_NoTmdbGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -360,6 +435,7 @@ namespace Ombi.Schedule.Tests
}); });
_mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>())) _mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 }); .ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object); await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never); _mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once); _mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
@ -368,9 +444,195 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
} }
[Test]
public async Task MovieRequestFromWatchList_NoTmdbGuid_LookupFromTdb()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "movie",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "imdb://123"
}
}
}
}
}
});
_mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
_mocker.Setup<IMovieDbApi, Task<FindResult>>(x => x.Find("123", ExternalSource.imdb_id)).ReturnsAsync(new FindResult
{
movie_results = new Movie_Results[]
{
new Movie_Results
{
id = 333
}
}
});
await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.Is<MovieRequestViewModel>(x => x.TheMovieDbId == 333)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<IMovieRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
_mocker.Verify<IMovieDbApi>(x => x.Find("123", ExternalSource.imdb_id), Times.Once);
}
[Test]
public async Task TvRequestFromWatchList_NoTmdbGuid_LookupFromTdb()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "show",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "imdbid://123"
}
}
}
}
}
});
_mocker.Setup<IMovieDbApi, Task<FindResult>>(x => x.Find("123", ExternalSource.imdb_id)).ReturnsAsync(new FindResult
{
tv_results = new TvResults[]
{
new TvResults
{
id = 333
}
}
});
_mocker.Setup<ITvRequestEngine, Task<RequestEngineResult>>(x => x.RequestTvShow(It.IsAny<TvRequestViewModelV2>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_mocker.Verify<ITvRequestEngine>(x => x.RequestTvShow(It.Is<TvRequestViewModelV2>(x => x.TheMovieDbId == 333 && x.LatestSeason == false && x.RequestAll == true)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<ITvRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
_mocker.Verify<IMovieDbApi>(x => x.Find("123", ExternalSource.imdb_id), Times.Once);
}
[Test]
public async Task TvRequestFromWatchList_NoTmdbGuid_LookupFromTdb_ViaTvDb()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "show",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "thetvdb://123"
}
}
}
}
}
});
_mocker.Setup<IMovieDbApi, Task<FindResult>>(x => x.Find("123", ExternalSource.tvdb_id)).ReturnsAsync(new FindResult
{
tv_results = new TvResults[]
{
new TvResults
{
id = 333
}
}
});
_mocker.Setup<ITvRequestEngine, Task<RequestEngineResult>>(x => x.RequestTvShow(It.IsAny<TvRequestViewModelV2>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_mocker.Verify<ITvRequestEngine>(x => x.RequestTvShow(It.Is<TvRequestViewModelV2>(x => x.TheMovieDbId == 333 && x.LatestSeason == false && x.RequestAll == true)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<ITvRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
_mocker.Verify<IMovieDbApi>(x => x.Find("123", ExternalSource.tvdb_id), Times.Once);
}
[Test] [Test]
public async Task TvRequestFromWatchList_NoTmdbGuid() public async Task TvRequestFromWatchList_NoTmdbGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -420,6 +682,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_AlreadyImported() public async Task MovieRequestFromWatchList_AlreadyImported()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -464,5 +727,56 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
} }
[Test]
public async Task TvRequestFromWatchList_RequestAllSeasons()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "show",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "tmdb://123"
}
}
}
}
}
});
_mocker.Setup<ITvRequestEngine, Task<RequestEngineResult>>(x => x.RequestTvShow(It.IsAny<TvRequestViewModelV2>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_mocker.Verify<ITvRequestEngine>(x => x.RequestTvShow(It.Is<TvRequestViewModelV2>(x => x.TheMovieDbId == 123 && x.LatestSeason == false && x.RequestAll == true)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<ITvRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
}
} }
} }

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Core; using Ombi.Core;
@ -11,7 +10,6 @@ using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Hubs; using Ombi.Hubs;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Plex.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;
@ -20,37 +18,30 @@ using Quartz;
namespace Ombi.Schedule.Jobs.Radarr namespace Ombi.Schedule.Jobs.Radarr
{ {
public class ArrAvailabilityChecker : IArrAvailabilityChecker public class ArrAvailabilityChecker : AvailabilityChecker, IArrAvailabilityChecker
{ {
private readonly IExternalRepository<RadarrCache> _radarrRepo; private readonly IExternalRepository<RadarrCache> _radarrRepo;
private readonly IExternalRepository<SonarrCache> _sonarrRepo; private readonly IExternalRepository<SonarrCache> _sonarrRepo;
private readonly ILogger<ArrAvailabilityChecker> _logger;
private readonly ISettingsService<RadarrSettings> _radarrSettings; private readonly ISettingsService<RadarrSettings> _radarrSettings;
private readonly ISettingsService<SonarrSettings> _sonarrSettings; private readonly ISettingsService<SonarrSettings> _sonarrSettings;
private readonly IExternalRepository<SonarrEpisodeCache> _sonarrEpisodeRepo; private readonly IExternalRepository<SonarrEpisodeCache> _sonarrEpisodeRepo;
private readonly INotificationHelper _notification;
private readonly IHubContext<NotificationHub> _hub;
private readonly ITvRequestRepository _tvRequest;
private readonly IMovieRequestRepository _movies; private readonly IMovieRequestRepository _movies;
public ArrAvailabilityChecker( public ArrAvailabilityChecker(
IExternalRepository<RadarrCache> radarrRepo, IExternalRepository<RadarrCache> radarrRepo,
IExternalRepository<SonarrCache> sonarrRepo, IExternalRepository<SonarrCache> sonarrRepo,
IExternalRepository<SonarrEpisodeCache> sonarrEpisodeRepo, IExternalRepository<SonarrEpisodeCache> sonarrEpisodeRepo,
INotificationHelper notification, IHubContext<NotificationHub> hub, INotificationHelper notification, INotificationHubService notificationHubService,
ITvRequestRepository tvRequest, IMovieRequestRepository movies, ITvRequestRepository tvRequest, IMovieRequestRepository movies,
ILogger<ArrAvailabilityChecker> log, ILogger<ArrAvailabilityChecker> log,
ISettingsService<RadarrSettings> radarrSettings, ISettingsService<RadarrSettings> radarrSettings,
ISettingsService<SonarrSettings> sonarrSettings) ISettingsService<SonarrSettings> sonarrSettings)
: base(tvRequest, notification, log, notificationHubService)
{ {
_radarrRepo = radarrRepo; _radarrRepo = radarrRepo;
_sonarrRepo = sonarrRepo; _sonarrRepo = sonarrRepo;
_sonarrEpisodeRepo = sonarrEpisodeRepo; _sonarrEpisodeRepo = sonarrEpisodeRepo;
_notification = notification;
_hub = hub;
_tvRequest = tvRequest;
_movies = movies; _movies = movies;
_logger = log;
_radarrSettings = radarrSettings; _radarrSettings = radarrSettings;
_sonarrSettings = sonarrSettings; _sonarrSettings = sonarrSettings;
} }
@ -82,7 +73,7 @@ namespace Ombi.Schedule.Jobs.Radarr
var available = availableRadarrMovies.FirstOrDefault(x => x.TheMovieDbId == movieRequest.TheMovieDbId); var available = availableRadarrMovies.FirstOrDefault(x => x.TheMovieDbId == movieRequest.TheMovieDbId);
if (available != null) if (available != null)
{ {
_logger.LogInformation($"Found move '{movieRequest.Title}' available in Radarr"); _log.LogInformation($"Found move '{movieRequest.Title}' available in Radarr");
if (available.Has4K && !movieRequest.Available4K) if (available.Has4K && !movieRequest.Available4K)
{ {
itemsForAvailability.Add(new AvailabilityModel itemsForAvailability.Add(new AvailabilityModel
@ -109,12 +100,12 @@ namespace Ombi.Schedule.Jobs.Radarr
if (itemsForAvailability.Any()) if (itemsForAvailability.Any())
{ {
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds) await NotificationHubService.SendNotificationToAdmins("Radarr Availability Checker found some new available movies!");
.SendAsync(NotificationHub.NotificationEvent, "Radarr Availability Checker found some new available movies!");
} }
foreach (var item in itemsForAvailability) foreach (var item in itemsForAvailability)
{ {
await _notification.Notify(new NotificationOptions await _notificationService.Notify(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable, NotificationType = NotificationType.RequestAvailable,
@ -127,9 +118,9 @@ namespace Ombi.Schedule.Jobs.Radarr
public async Task ProcessTvShows() public async Task ProcessTvShows()
{ {
var tv = await _tvRequest.GetChild().Where(x => !x.Available).ToListAsync(); var tv = await _tvRepo.GetChild().Where(x => !x.Available).ToListAsync();
var sonarrEpisodes = _sonarrEpisodeRepo.GetAll().Where(x => x.HasFile); var sonarrEpisodes = _sonarrEpisodeRepo.GetAll().Where(x => x.HasFile);
foreach (var child in tv) foreach (var child in tv)
{ {
var tvDbId = child.ParentRequest.TvDbId; var tvDbId = child.ParentRequest.TvDbId;
@ -140,83 +131,10 @@ namespace Ombi.Schedule.Jobs.Radarr
continue; continue;
} }
//if (!seriesEpisodes.Any()) await ProcessTvShow(seriesEpisodes, child);
//{
// // Let's try and match the series by name
// seriesEpisodes = sonarrEpisodes.Where(x =>
// x.EpisodeNumber == child.Title &&
// x.Series.ReleaseYear == child.ParentRequest.ReleaseDate.Year.ToString());
//}
var availableEpisode = new List<AvailabilityModel>();
foreach (var season in child.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
if (episode.Available)
{
continue;
}
var foundEp = await seriesEpisodes.AnyAsync(
x => x.EpisodeNumber == episode.EpisodeNumber &&
x.SeasonNumber == episode.Season.SeasonNumber);
if (foundEp)
{
availableEpisode.Add(new AvailabilityModel
{
Id = episode.Id,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = episode.Season.SeasonNumber
});
episode.Available = true;
}
}
}
if (availableEpisode.Any())
{
await _tvRequest.Save();
}
// Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable)
{
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Sonarr Availability Checker found some new available Shows!");
child.Available = true;
child.MarkedAsAvailable = DateTime.UtcNow;
_logger.LogInformation("[ARR_AC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}");
// We have ful-fulled this request!
await _tvRequest.Save();
await _notification.Notify(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email
});
}
else if (availableEpisode.Any())
{
var notification = new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.PartiallyAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email,
};
notification.Substitutes.Add("Season", availableEpisode.First().SeasonNumber.ToString());
notification.Substitutes.Add("Episodes", string.Join(", ", availableEpisode.Select(x => x.EpisodeNumber)));
await _notification.Notify(notification);
}
} }
await _tvRequest.Save(); await _tvRepo.Save();
} }
private bool _disposed; private bool _disposed;

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Notifications.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs
{
public abstract class AvailabilityChecker
{
protected readonly ITvRequestRepository _tvRepo;
protected readonly INotificationHelper _notificationService;
protected readonly ILogger _log;
protected readonly INotificationHubService NotificationHubService;
public AvailabilityChecker(ITvRequestRepository tvRequest, INotificationHelper notification,
ILogger log, INotificationHubService notificationHubService)
{
_tvRepo = tvRequest;
_notificationService = notification;
_log = log;
NotificationHubService = notificationHubService;
}
protected async Task ProcessTvShow(IQueryable<IBaseMediaServerEpisode> seriesEpisodes, ChildRequests child)
{
var availableEpisode = new List<AvailabilityModel>();
foreach (var season in child.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
if (episode.Available)
{
continue;
}
var foundEp = await seriesEpisodes.AnyAsync(
x => x.EpisodeNumber == episode.EpisodeNumber &&
x.SeasonNumber == episode.Season.SeasonNumber);
if (foundEp)
{
availableEpisode.Add(new AvailabilityModel
{
Id = episode.Id,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = episode.Season.SeasonNumber
});
episode.Available = true;
}
}
}
if (availableEpisode.Any())
{
await _tvRepo.Save();
}
// Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable)
{
// We have ful-fulled this request!
child.Available = true;
child.MarkedAsAvailable = DateTime.UtcNow;
await NotificationHubService.SendNotificationToAdmins("Availability Checker found some new available Shows!");
_log.LogInformation("Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}");
await _tvRepo.Save();
await _notificationService.Notify(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email
});
}
else if (availableEpisode.Any())
{
var notification = new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.PartiallyAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email,
};
notification.Substitutes.Add("Season", availableEpisode.First().SeasonNumber.ToString());
notification.Substitutes.Add("Episodes", string.Join(", ", availableEpisode.Select(x => x.EpisodeNumber)));
notification.Substitutes.Add("EpisodesCount", $"{availableEpisode.Count}");
notification.Substitutes.Add("SeasonEpisodes", string.Join(", ", availableEpisode.Select(x => $"{x.SeasonNumber}x{x.EpisodeNumber}")));
await _notificationService.Notify(notification);
}
}
}
}

@ -28,7 +28,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.CouchPotato; using Ombi.Api.CouchPotato;
@ -45,13 +44,13 @@ namespace Ombi.Schedule.Jobs.Couchpotato
public class CouchPotatoSync : ICouchPotatoSync public class CouchPotatoSync : ICouchPotatoSync
{ {
public CouchPotatoSync(ISettingsService<CouchPotatoSettings> cpSettings, public CouchPotatoSync(ISettingsService<CouchPotatoSettings> cpSettings,
ICouchPotatoApi api, ILogger<CouchPotatoSync> log, ExternalContext ctx, IHubContext<NotificationHub> hub) ICouchPotatoApi api, ILogger<CouchPotatoSync> log, ExternalContext ctx, INotificationHubService notificationHubService)
{ {
_settings = cpSettings; _settings = cpSettings;
_api = api; _api = api;
_log = log; _log = log;
_ctx = ctx; _ctx = ctx;
_notification = hub; _notification = notificationHubService;
_settings.ClearCache(); _settings.ClearCache();
} }
@ -59,7 +58,7 @@ namespace Ombi.Schedule.Jobs.Couchpotato
private readonly ICouchPotatoApi _api; private readonly ICouchPotatoApi _api;
private readonly ILogger<CouchPotatoSync> _log; private readonly ILogger<CouchPotatoSync> _log;
private readonly ExternalContext _ctx; private readonly ExternalContext _ctx;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
{ {
@ -69,8 +68,7 @@ namespace Ombi.Schedule.Jobs.Couchpotato
return; return;
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Couch Potato Sync Started");
.SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Started");
try try
{ {
_log.LogInformation(LoggingEvents.CouchPotatoCacher, "Getting all active movies from CP"); _log.LogInformation(LoggingEvents.CouchPotatoCacher, "Getting all active movies from CP");
@ -81,11 +79,9 @@ namespace Ombi.Schedule.Jobs.Couchpotato
await strat.ExecuteAsync(async () => await strat.ExecuteAsync(async () =>
{ {
// Let's remove the old cached data // Let's remove the old cached data
using (var tran = await _ctx.Database.BeginTransactionAsync()) using var tran = await _ctx.Database.BeginTransactionAsync();
{ await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache");
await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); await tran.CommitAsync();
tran.Commit();
}
}); });
// Save // Save
@ -109,23 +105,19 @@ namespace Ombi.Schedule.Jobs.Couchpotato
strat = _ctx.Database.CreateExecutionStrategy(); strat = _ctx.Database.CreateExecutionStrategy();
await strat.ExecuteAsync(async () => await strat.ExecuteAsync(async () =>
{ {
using (var tran = await _ctx.Database.BeginTransactionAsync()) using var tran = await _ctx.Database.BeginTransactionAsync();
{ await _ctx.CouchPotatoCache.AddRangeAsync(movieIds);
await _ctx.CouchPotatoCache.AddRangeAsync(movieIds);
await _ctx.SaveChangesAsync(); await _ctx.SaveChangesAsync();
tran.Commit(); await tran.CommitAsync();
}
}); });
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Couch Potato Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Finished");
} }
} }
catch (Exception e) catch (Exception e)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Couch Potato Sync Failed");
.SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Failed");
_log.LogError(LoggingEvents.CouchPotatoCacher, e, "error when trying to get movies from CP"); _log.LogError(LoggingEvents.CouchPotatoCacher, e, "error when trying to get movies from CP");
throw; throw;
} }

@ -1,17 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
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.Notifications;
using Ombi.Core.Services; 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.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -20,39 +16,30 @@ using Quartz;
namespace Ombi.Schedule.Jobs.Emby namespace Ombi.Schedule.Jobs.Emby
{ {
public class EmbyAvaliabilityChecker : IEmbyAvaliabilityChecker public class EmbyAvaliabilityChecker : AvailabilityChecker, 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, IFeatureService featureService) INotificationHelper n, ILogger<EmbyAvaliabilityChecker> log, INotificationHubService notification, IFeatureService featureService)
: base(t, n, log, notification)
{ {
_repo = repo; _repo = repo;
_tvRepo = t;
_movieRepo = m; _movieRepo = m;
_notificationService = n;
_log = log;
_notification = notification;
_featureService = featureService; _featureService = featureService;
} }
private readonly ITvRequestRepository _tvRepo;
private readonly IMovieRequestRepository _movieRepo; private readonly IMovieRequestRepository _movieRepo;
private readonly IEmbyContentRepository _repo; private readonly IEmbyContentRepository _repo;
private readonly INotificationHelper _notificationService;
private readonly ILogger<EmbyAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _notification;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
{ {
_log.LogInformation("Starting Emby Availability Check"); _log.LogInformation("Starting Emby Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await NotificationHubService.SendNotificationToAdmins("Emby Availability Checker Started");
.SendAsync(NotificationHub.NotificationEvent, "Emby Availability Checker Started");
await ProcessMovies(); await ProcessMovies();
await ProcessTv(); await ProcessTv();
_log.LogInformation("Finished Emby Availability Check"); _log.LogInformation("Finished Emby Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await NotificationHubService.SendNotificationToAdmins("Emby Availability Checker Finished");
.SendAsync(NotificationHub.NotificationEvent, "Emby Availability Checker Finished");
} }
private async Task ProcessMovies() private async Task ProcessMovies()
@ -167,68 +154,7 @@ namespace Ombi.Schedule.Jobs.Emby
x.Series.Title == child.Title); x.Series.Title == child.Title);
} }
var availableEpisode = new List<AvailabilityModel>(); await ProcessTvShow(seriesEpisodes, child);
foreach (var season in child.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
if (episode.Available)
{
continue;
}
var foundEp = await seriesEpisodes.FirstOrDefaultAsync(
x => x.EpisodeNumber == episode.EpisodeNumber &&
x.SeasonNumber == episode.Season.SeasonNumber);
if (foundEp != null)
{
availableEpisode.Add(new AvailabilityModel
{
Id = episode.Id,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = episode.Season.SeasonNumber
});
episode.Available = true;
}
}
}
if (availableEpisode.Any())
{
await _tvRepo.Save();
}
// Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable)
{
// We have fulfulled this request!
child.Available = true;
child.MarkedAsAvailable = DateTime.Now;
await _notificationService.Notify(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email
});
}
else if (availableEpisode.Any())
{
var notification = new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.PartiallyAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email,
};
notification.Substitutes.Add("Season", availableEpisode.First().SeasonNumber.ToString());
notification.Substitutes.Add("Episodes", string.Join(", ", availableEpisode.Select(x => x.EpisodeNumber)));
await _notificationService.Notify(notification);
}
} }
await _tvRepo.Save(); await _tvRepo.Save();

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Emby; using Ombi.Api.Emby;
using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models;
@ -13,7 +12,6 @@ using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External; using Ombi.Core.Settings.Models.External;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Hubs; using Ombi.Hubs;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Quartz; using Quartz;
@ -24,7 +22,7 @@ namespace Ombi.Schedule.Jobs.Emby
public class EmbyContentSync : IEmbyContentSync public class EmbyContentSync : IEmbyContentSync
{ {
public EmbyContentSync(ISettingsService<EmbySettings> settings, IEmbyApiFactory api, ILogger<EmbyContentSync> logger, public EmbyContentSync(ISettingsService<EmbySettings> settings, IEmbyApiFactory api, ILogger<EmbyContentSync> logger,
IEmbyContentRepository repo, IHubContext<NotificationHub> notification) IEmbyContentRepository repo, INotificationHubService notification)
{ {
_logger = logger; _logger = logger;
_settings = settings; _settings = settings;
@ -37,7 +35,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly ISettingsService<EmbySettings> _settings; private readonly ISettingsService<EmbySettings> _settings;
private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyApiFactory _apiFactory;
private readonly IEmbyContentRepository _repo; private readonly IEmbyContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
private const int AmountToTake = 100; private const int AmountToTake = 100;
@ -58,8 +56,7 @@ namespace Ombi.Schedule.Jobs.Emby
Api = _apiFactory.CreateClient(embySettings); Api = _apiFactory.CreateClient(embySettings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins(recentlyAddedSearch ? "Emby Recently Added Started" : "Emby Content Sync Started");
.SendAsync(NotificationHub.NotificationEvent, recentlyAddedSearch ? "Emby Recently Added Started" : "Emby Content Sync Started");
foreach (var server in embySettings.Servers) foreach (var server in embySettings.Servers)
{ {
@ -69,14 +66,12 @@ namespace Ombi.Schedule.Jobs.Emby
} }
catch (Exception e) catch (Exception e)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Emby Content Sync Failed");
.SendAsync(NotificationHub.NotificationEvent, "Emby Content Sync Failed");
_logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name);
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Emby Content Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Emby Content Sync Finished");
// Episodes // Episodes

@ -29,7 +29,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Emby; using Ombi.Api.Emby;
using Ombi.Core.Settings; using Ombi.Core.Settings;
@ -48,7 +47,7 @@ namespace Ombi.Schedule.Jobs.Emby
public class EmbyEpisodeSync : IEmbyEpisodeSync public class EmbyEpisodeSync : IEmbyEpisodeSync
{ {
public EmbyEpisodeSync(ISettingsService<EmbySettings> s, IEmbyApiFactory api, ILogger<EmbyEpisodeSync> l, IEmbyContentRepository repo public EmbyEpisodeSync(ISettingsService<EmbySettings> s, IEmbyApiFactory api, ILogger<EmbyEpisodeSync> l, IEmbyContentRepository repo
, IHubContext<NotificationHub> notification) , INotificationHubService notification)
{ {
_apiFactory = api; _apiFactory = api;
_logger = l; _logger = l;
@ -61,7 +60,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyApiFactory _apiFactory;
private readonly ILogger<EmbyEpisodeSync> _logger; private readonly ILogger<EmbyEpisodeSync> _logger;
private readonly IEmbyContentRepository _repo; private readonly IEmbyContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
private const int AmountToTake = 100; private const int AmountToTake = 100;
@ -80,8 +79,7 @@ namespace Ombi.Schedule.Jobs.Emby
var settings = await _settings.GetSettingsAsync(); var settings = await _settings.GetSettingsAsync();
Api = _apiFactory.CreateClient(settings); Api = _apiFactory.CreateClient(settings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Emby Episode Sync Started");
.SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started");
foreach (var server in settings.Servers) foreach (var server in settings.Servers)
{ {
if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled))
@ -99,8 +97,7 @@ namespace Ombi.Schedule.Jobs.Emby
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Emby Episode Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Finished");
_logger.LogInformation("Emby Episode Sync Finished - Triggering Metadata refresh"); _logger.LogInformation("Emby Episode Sync Finished - Triggering Metadata refresh");
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
} }

@ -29,7 +29,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Emby; using Ombi.Api.Emby;
@ -46,7 +45,7 @@ namespace Ombi.Schedule.Jobs.Emby
public class EmbyUserImporter : IEmbyUserImporter public class EmbyUserImporter : IEmbyUserImporter
{ {
public EmbyUserImporter(IEmbyApiFactory api, UserManager<OmbiUser> um, ILogger<EmbyUserImporter> log, public EmbyUserImporter(IEmbyApiFactory api, UserManager<OmbiUser> um, ILogger<EmbyUserImporter> log,
ISettingsService<EmbySettings> embySettings, ISettingsService<UserManagementSettings> ums, IHubContext<NotificationHub> notification) ISettingsService<EmbySettings> embySettings, ISettingsService<UserManagementSettings> ums, INotificationHubService notification)
{ {
_apiFactory = api; _apiFactory = api;
_userManager = um; _userManager = um;
@ -61,7 +60,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly ILogger<EmbyUserImporter> _log; private readonly ILogger<EmbyUserImporter> _log;
private readonly ISettingsService<EmbySettings> _embySettings; private readonly ISettingsService<EmbySettings> _embySettings;
private readonly ISettingsService<UserManagementSettings> _userManagementSettings; private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
private IEmbyApi Api { get; set; } private IEmbyApi Api { get; set; }
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
@ -79,8 +78,7 @@ namespace Ombi.Schedule.Jobs.Emby
Api = _apiFactory.CreateClient(settings); Api = _apiFactory.CreateClient(settings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Emby User Importer Started");
.SendAsync(NotificationHub.NotificationEvent, $"Emby User Importer Started");
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync();
foreach (var server in settings.Servers) foreach (var server in settings.Servers)
{ {
@ -160,8 +158,7 @@ namespace Ombi.Schedule.Jobs.Emby
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Emby User Importer Finished");
.SendAsync(NotificationHub.NotificationEvent, "Emby User Importer Finished");
} }
private bool _disposed; private bool _disposed;

@ -26,10 +26,8 @@
#endregion #endregion
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Core; using Ombi.Core;
@ -45,39 +43,30 @@ using Quartz;
namespace Ombi.Schedule.Jobs.Jellyfin namespace Ombi.Schedule.Jobs.Jellyfin
{ {
public class JellyfinAvaliabilityChecker : IJellyfinAvaliabilityChecker public class JellyfinAvaliabilityChecker : AvailabilityChecker, 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, IFeatureService featureService) INotificationHelper n, ILogger<JellyfinAvaliabilityChecker> log, INotificationHubService notification, IFeatureService featureService)
: base(t, n, log, notification)
{ {
_repo = repo; _repo = repo;
_tvRepo = t;
_movieRepo = m; _movieRepo = m;
_notificationService = n;
_log = log;
_notification = notification;
_featureService = featureService; _featureService = featureService;
} }
private readonly ITvRequestRepository _tvRepo;
private readonly IMovieRequestRepository _movieRepo; private readonly IMovieRequestRepository _movieRepo;
private readonly IJellyfinContentRepository _repo; private readonly IJellyfinContentRepository _repo;
private readonly INotificationHelper _notificationService;
private readonly ILogger<JellyfinAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _notification;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
{ {
_log.LogInformation("Starting Jellyfin Availability Check"); _log.LogInformation("Starting Jellyfin Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await NotificationHubService.SendNotificationToAdmins("Jellyfin Availability Checker Started");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Started");
await ProcessMovies(); await ProcessMovies();
await ProcessTv(); await ProcessTv();
_log.LogInformation("Finished Jellyfin Availability Check"); _log.LogInformation("Finished Jellyfin Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await NotificationHubService.SendNotificationToAdmins("Jellyfin Availability Checker Finished");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Finished");
} }
private async Task ProcessMovies() private async Task ProcessMovies()
@ -193,68 +182,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
x.Series.Title == child.Title); x.Series.Title == child.Title);
} }
var availableEpisode = new List<AvailabilityModel>(); await ProcessTvShow(seriesEpisodes, child);
foreach (var season in child.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
if (episode.Available)
{
continue;
}
var foundEp = await seriesEpisodes.FirstOrDefaultAsync(
x => x.EpisodeNumber == episode.EpisodeNumber &&
x.SeasonNumber == episode.Season.SeasonNumber);
if (foundEp != null)
{
availableEpisode.Add(new AvailabilityModel
{
Id = episode.Id,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = episode.Season.SeasonNumber
});
episode.Available = true;
}
}
}
if (availableEpisode.Any())
{
await _tvRepo.Save();
}
// Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable)
{
// We have fulfulled this request!
child.Available = true;
child.MarkedAsAvailable = DateTime.Now;
await _notificationService.Notify(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email
});
}
else if (availableEpisode.Any())
{
var notification = new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.PartiallyAvailable,
RequestId = child.Id,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email,
};
notification.Substitutes.Add("Season", availableEpisode.First().SeasonNumber.ToString());
notification.Substitutes.Add("Episodes", string.Join(", ", availableEpisode.Select(x => x.EpisodeNumber)));
await _notificationService.Notify(notification);
}
} }
await _tvRepo.Save(); await _tvRepo.Save();

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Jellyfin; using Ombi.Api.Jellyfin;
using Ombi.Api.Jellyfin.Models.Movie; using Ombi.Api.Jellyfin.Models.Movie;
@ -11,7 +10,6 @@ using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External; using Ombi.Core.Settings.Models.External;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Hubs; using Ombi.Hubs;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Quartz; using Quartz;
@ -22,7 +20,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
public class JellyfinContentSync : IJellyfinContentSync public class JellyfinContentSync : IJellyfinContentSync
{ {
public JellyfinContentSync(ISettingsService<JellyfinSettings> settings, IJellyfinApiFactory api, ILogger<JellyfinContentSync> logger, public JellyfinContentSync(ISettingsService<JellyfinSettings> settings, IJellyfinApiFactory api, ILogger<JellyfinContentSync> logger,
IJellyfinContentRepository repo, IHubContext<NotificationHub> notification) IJellyfinContentRepository repo, INotificationHubService notification)
{ {
_logger = logger; _logger = logger;
_settings = settings; _settings = settings;
@ -35,7 +33,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private readonly ISettingsService<JellyfinSettings> _settings; private readonly ISettingsService<JellyfinSettings> _settings;
private readonly IJellyfinApiFactory _apiFactory; private readonly IJellyfinApiFactory _apiFactory;
private readonly IJellyfinContentRepository _repo; private readonly IJellyfinContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
private IJellyfinApi Api { get; set; } private IJellyfinApi Api { get; set; }
@ -47,8 +45,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
Api = _apiFactory.CreateClient(jellyfinSettings); Api = _apiFactory.CreateClient(jellyfinSettings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin Content Sync Started");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Started");
foreach (var server in jellyfinSettings.Servers) foreach (var server in jellyfinSettings.Servers)
{ {
@ -58,13 +55,11 @@ namespace Ombi.Schedule.Jobs.Jellyfin
} }
catch (Exception e) catch (Exception e)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin Content Sync Failed");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Failed");
_logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name); _logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name);
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin Content Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished");
// Episodes // Episodes
await OmbiQuartz.TriggerJob(nameof(IJellyfinEpisodeSync), "Jellyfin"); await OmbiQuartz.TriggerJob(nameof(IJellyfinEpisodeSync), "Jellyfin");

@ -29,7 +29,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Jellyfin; using Ombi.Api.Jellyfin;
using Ombi.Core.Settings; using Ombi.Core.Settings;
@ -46,7 +45,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
public class JellyfinEpisodeSync : IJellyfinEpisodeSync public class JellyfinEpisodeSync : IJellyfinEpisodeSync
{ {
public JellyfinEpisodeSync(ISettingsService<JellyfinSettings> s, IJellyfinApiFactory api, ILogger<JellyfinEpisodeSync> l, IJellyfinContentRepository repo public JellyfinEpisodeSync(ISettingsService<JellyfinSettings> s, IJellyfinApiFactory api, ILogger<JellyfinEpisodeSync> l, IJellyfinContentRepository repo
, IHubContext<NotificationHub> notification) , INotificationHubService notification)
{ {
_apiFactory = api; _apiFactory = api;
_logger = l; _logger = l;
@ -59,7 +58,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private readonly IJellyfinApiFactory _apiFactory; private readonly IJellyfinApiFactory _apiFactory;
private readonly ILogger<JellyfinEpisodeSync> _logger; private readonly ILogger<JellyfinEpisodeSync> _logger;
private readonly IJellyfinContentRepository _repo; private readonly IJellyfinContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
private IJellyfinApi Api { get; set; } private IJellyfinApi Api { get; set; }
@ -68,8 +67,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
var settings = await _settings.GetSettingsAsync(); var settings = await _settings.GetSettingsAsync();
Api = _apiFactory.CreateClient(settings); Api = _apiFactory.CreateClient(settings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin Episode Sync Started");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started");
foreach (var server in settings.Servers) foreach (var server in settings.Servers)
{ {
@ -88,8 +86,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin Episode Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Finished");
_logger.LogInformation("Jellyfin Episode Sync Finished - Triggering Metadata refresh"); _logger.LogInformation("Jellyfin Episode Sync Finished - Triggering Metadata refresh");
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
} }

@ -29,7 +29,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Jellyfin; using Ombi.Api.Jellyfin;
@ -46,7 +45,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
public class JellyfinUserImporter : IJellyfinUserImporter public class JellyfinUserImporter : IJellyfinUserImporter
{ {
public JellyfinUserImporter(IJellyfinApiFactory api, UserManager<OmbiUser> um, ILogger<JellyfinUserImporter> log, public JellyfinUserImporter(IJellyfinApiFactory api, UserManager<OmbiUser> um, ILogger<JellyfinUserImporter> log,
ISettingsService<JellyfinSettings> jellyfinSettings, ISettingsService<UserManagementSettings> ums, IHubContext<NotificationHub> notification) ISettingsService<JellyfinSettings> jellyfinSettings, ISettingsService<UserManagementSettings> ums, INotificationHubService notification)
{ {
_apiFactory = api; _apiFactory = api;
_userManager = um; _userManager = um;
@ -61,7 +60,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private readonly ILogger<JellyfinUserImporter> _log; private readonly ILogger<JellyfinUserImporter> _log;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings; private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly ISettingsService<UserManagementSettings> _userManagementSettings; private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
private IJellyfinApi Api { get; set; } private IJellyfinApi Api { get; set; }
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
@ -79,8 +78,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
Api = _apiFactory.CreateClient(settings); Api = _apiFactory.CreateClient(settings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin User Importer Started");
.SendAsync(NotificationHub.NotificationEvent, $"Jellyfin User Importer Started");
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.JellyfinUser).ToListAsync(); var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.JellyfinUser).ToListAsync();
foreach (var server in settings.Servers) foreach (var server in settings.Servers)
{ {
@ -146,8 +144,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Jellyfin User Importer Finished");
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin User Importer Finished");
} }
private bool _disposed; private bool _disposed;

@ -2,9 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr; using Ombi.Api.Lidarr;
using Ombi.Core.Settings; using Ombi.Core.Settings;
@ -21,7 +19,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
public class LidarrAlbumSync : ILidarrAlbumSync public class LidarrAlbumSync : ILidarrAlbumSync
{ {
public LidarrAlbumSync(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<LidarrAlbumSync> log, ExternalContext ctx, public LidarrAlbumSync(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<LidarrAlbumSync> log, ExternalContext ctx,
IHubContext<NotificationHub> notification) INotificationHubService notification)
{ {
_lidarrSettings = lidarr; _lidarrSettings = lidarr;
_lidarrApi = lidarrApi; _lidarrApi = lidarrApi;
@ -34,7 +32,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
private readonly ILidarrApi _lidarrApi; private readonly ILidarrApi _lidarrApi;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ExternalContext _ctx; private readonly ExternalContext _ctx;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
public async Task Execute(IJobExecutionContext ctx) public async Task Execute(IJobExecutionContext ctx)
{ {
@ -44,8 +42,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
if (settings.Enabled) if (settings.Enabled)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Lidarr Album Sync Started");
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Album Sync Started");
try try
{ {
var albums = await _lidarrApi.GetAllAlbums(settings.ApiKey, settings.FullUri); var albums = await _lidarrApi.GetAllAlbums(settings.ApiKey, settings.FullUri);
@ -58,7 +55,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
using (var tran = await _ctx.Database.BeginTransactionAsync()) using (var tran = await _ctx.Database.BeginTransactionAsync())
{ {
await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrAlbumCache"); await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrAlbumCache");
tran.Commit(); await tran.CommitAsync();
} }
}); });
@ -88,20 +85,18 @@ namespace Ombi.Schedule.Jobs.Lidarr
await _ctx.LidarrAlbumCache.AddRangeAsync(albumCache); await _ctx.LidarrAlbumCache.AddRangeAsync(albumCache);
await _ctx.SaveChangesAsync(); await _ctx.SaveChangesAsync();
tran.Commit(); await tran.CommitAsync();
} }
}); });
} }
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Lidarr Album Sync Failed");
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Album Sync Failed");
_logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Lidarr Album"); _logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Lidarr Album");
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Lidarr Album Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Album Sync Finished");
await OmbiQuartz.TriggerJob(nameof(ILidarrAvailabilityChecker), "DVR"); await OmbiQuartz.TriggerJob(nameof(ILidarrAvailabilityChecker), "DVR");
} }

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr; using Ombi.Api.Lidarr;
@ -20,7 +19,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
public class LidarrArtistSync : ILidarrArtistSync public class LidarrArtistSync : ILidarrArtistSync
{ {
public LidarrArtistSync(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<LidarrArtistSync> log, ExternalContext ctx public LidarrArtistSync(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<LidarrArtistSync> log, ExternalContext ctx
, IHubContext<NotificationHub> notification) , INotificationHubService notification)
{ {
_lidarrSettings = lidarr; _lidarrSettings = lidarr;
_lidarrApi = lidarrApi; _lidarrApi = lidarrApi;
@ -33,7 +32,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
private readonly ILidarrApi _lidarrApi; private readonly ILidarrApi _lidarrApi;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ExternalContext _ctx; private readonly ExternalContext _ctx;
private readonly IHubContext<NotificationHub> _notification; private readonly INotificationHubService _notification;
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
{ {
@ -43,8 +42,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
if (settings.Enabled) if (settings.Enabled)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Lidarr Artist Sync Started");
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Artist Sync Started");
try try
{ {
var artists = await _lidarrApi.GetArtists(settings.ApiKey, settings.FullUri); var artists = await _lidarrApi.GetArtists(settings.ApiKey, settings.FullUri);
@ -54,11 +52,9 @@ namespace Ombi.Schedule.Jobs.Lidarr
await strat.ExecuteAsync(async () => await strat.ExecuteAsync(async () =>
{ {
// Let's remove the old cached data // Let's remove the old cached data
using (var tran = await _ctx.Database.BeginTransactionAsync()) using var tran = await _ctx.Database.BeginTransactionAsync();
{ await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrArtistCache");
await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrArtistCache"); await tran.CommitAsync();
tran.Commit();
}
}); });
var artistCache = new List<LidarrArtistCache>(); var artistCache = new List<LidarrArtistCache>();
@ -78,25 +74,21 @@ namespace Ombi.Schedule.Jobs.Lidarr
strat = _ctx.Database.CreateExecutionStrategy(); strat = _ctx.Database.CreateExecutionStrategy();
await strat.ExecuteAsync(async () => await strat.ExecuteAsync(async () =>
{ {
using (var tran = await _ctx.Database.BeginTransactionAsync()) using var tran = await _ctx.Database.BeginTransactionAsync();
{ await _ctx.LidarrArtistCache.AddRangeAsync(artistCache);
await _ctx.LidarrArtistCache.AddRangeAsync(artistCache);
await _ctx.SaveChangesAsync(); await _ctx.SaveChangesAsync();
tran.Commit(); await tran.CommitAsync();
}
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Lidarr Artist Sync Failed");
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Artist Sync Failed");
_logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Lidarr"); _logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Lidarr");
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.SendNotificationToAdmins("Lidarr Artist Sync Finished");
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Artist Sync Finished");
await OmbiQuartz.TriggerJob(nameof(ILidarrAlbumSync), "DVR"); await OmbiQuartz.TriggerJob(nameof(ILidarrAlbumSync), "DVR");
} }

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

Loading…
Cancel
Save