* 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/setup-node@v2
with:
node-version: '14'
node-version: '18'
- name: NodeModules Cache
uses: actions/cache@v2

@ -22,7 +22,7 @@ jobs:
dotnet-version: 6.0.x
- uses: actions/setup-node@v2
with:
node-version: '14'
node-version: '18'
- uses: actions/cache@v2
with:
@ -43,6 +43,9 @@ jobs:
- name: Run Docker Image
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
run: sleep 20

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

3
.gitignore vendored

@ -7,7 +7,7 @@
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
# User-specific files
*.userprefs
# Build results
@ -252,3 +252,4 @@ _Pvt_Extensions
/src/Ombi/healthchecksdb
/src/Ombi/ClientApp/package-lock.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
* Set the default job for the watchlist import to hourly instead of daily ([75906af](https://github.com/Ombi-app/Ombi/commit/75906af0adee3e3c68d825c3aaa8f7b918461b1f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4fc2c1f](https://github.com/Ombi-app/Ombi/commit/4fc2c1f24534085a783a3d5791f5533b68272153))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76ab733](https://github.com/Ombi-app/Ombi/commit/76ab733b91791e4d93d184f3c7d0779c6a388695))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([06e4cef](https://github.com/Ombi-app/Ombi/commit/06e4cefa7b4e55b860da9a64f461f6ec8fa17367))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c12d89d](https://github.com/Ombi-app/Ombi/commit/c12d89d6781a337520977ad285f8d08c93f434dd))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bc0c2f6](https://github.com/Ombi-app/Ombi/commit/bc0c2f622e34fb5a2711039d9ed7aad34f982b15))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([e4b00e6](https://github.com/Ombi-app/Ombi/commit/e4b00e6b3468bd9389eeb02fc6ad7daf27abc3b3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1998d3](https://github.com/Ombi-app/Ombi/commit/d1998d326f999a38586d0a351a20c5448df95842))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bee4ccb](https://github.com/Ombi-app/Ombi/commit/bee4ccb804594e7385b1fbdc9fe2ef5c42e0d21f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([80233ed](https://github.com/Ombi-app/Ombi/commit/80233ed560cc976e83570d0655c3472f20171fb3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a78adc](https://github.com/Ombi-app/Ombi/commit/8a78adc9bb62f277f2b213dcb3847ed6d0089fcb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d04c60a](https://github.com/Ombi-app/Ombi/commit/d04c60aa5909b47ba6bffa6f66b03079cbd43521))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([92a785e](https://github.com/Ombi-app/Ombi/commit/92a785e736fa4b72a45270da2d0f4661df433078))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([634982d](https://github.com/Ombi-app/Ombi/commit/634982df2661cefab5ea9f5163fe04a005cc0171))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([b404baa](https://github.com/Ombi-app/Ombi/commit/b404baad6d0aeaa1561701e0db8db4e78613a364))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d14f11e](https://github.com/Ombi-app/Ombi/commit/d14f11e0eb20ab0a68e765ee77968b3b3e54e995))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7cf64f9](https://github.com/Ombi-app/Ombi/commit/7cf64f909d78908edaabeffb8a39a7d02e73fe7e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c9e1ec](https://github.com/Ombi-app/Ombi/commit/0c9e1ec090827080cc8f7393e5e91456ff37d691))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([3b0b730](https://github.com/Ombi-app/Ombi/commit/3b0b730cb02efe24f6d4026e5fdb20d37e495119))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6ed1a03](https://github.com/Ombi-app/Ombi/commit/6ed1a03b7ff4077f09ea9e13394b18b0d138f4c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([2941acd](https://github.com/Ombi-app/Ombi/commit/2941acd3b2ec74a5e6aeea275ab5a39d2653f37f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c075a1a](https://github.com/Ombi-app/Ombi/commit/c075a1a66784d975eaf60f2dfbbcbe048f2f63d7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76bd81c](https://github.com/Ombi-app/Ombi/commit/76bd81c3ca55a98c6ec944a838dc01294a6193a6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0d38275](https://github.com/Ombi-app/Ombi/commit/0d3827507e002bcf58f673e97ffcc3bd25dcf337))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5c99601](https://github.com/Ombi-app/Ombi/commit/5c99601b07aec1a65d0186a4c4327440811e64c6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01546a0](https://github.com/Ombi-app/Ombi/commit/01546a0f7f86379528b486463246ef9bdfb9033e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d7fea78](https://github.com/Ombi-app/Ombi/commit/d7fea7843aaaab7ddff8dc31ca6d2a9117471dcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([1a6b95d](https://github.com/Ombi-app/Ombi/commit/1a6b95d45c220310213b8d811272a63f0f6ff42b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([fa10174](https://github.com/Ombi-app/Ombi/commit/fa1017422c4efd4b0897871bd3c671151774d7c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c31e62](https://github.com/Ombi-app/Ombi/commit/0c31e628df376aac6d56ae67c7c705a9a4a7c080))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6399643](https://github.com/Ombi-app/Ombi/commit/63996437a02fe10ffae6822ffa15369bec0a6b36))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5826e2d](https://github.com/Ombi-app/Ombi/commit/5826e2d9a1c3f1210a87fa270dc0c81bac32944a))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d434514](https://github.com/Ombi-app/Ombi/commit/d43451405be489254d7cdc7755d5f516a1e495a5))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0b9596d](https://github.com/Ombi-app/Ombi/commit/0b9596d807178f5e071113ec0347868ec7f0960b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8c4c0b2](https://github.com/Ombi-app/Ombi/commit/8c4c0b262978c1303767af360d802c4b4c2b4d24))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([289ab77](https://github.com/Ombi-app/Ombi/commit/289ab77b0e04aae235b6f6cebc86e0a8d1f0cf2b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([30e3417](https://github.com/Ombi-app/Ombi/commit/30e3417285a4eed18d429d7776f0e74096e834c0))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6c0a5da](https://github.com/Ombi-app/Ombi/commit/6c0a5dadd4b8f37760252eb0fe7f88908f55506d))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d5bf969](https://github.com/Ombi-app/Ombi/commit/d5bf9692ce1fc0ccfe7beca6dd200c78be177bdc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a9e7ea](https://github.com/Ombi-app/Ombi/commit/8a9e7ea588aefbcd73ed82625887e3614e1703ea))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01047a3](https://github.com/Ombi-app/Ombi/commit/01047a3fd67153f3ff16f860d2c7b50213e8d9b2))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([698a23f](https://github.com/Ombi-app/Ombi/commit/698a23fb83f323cdd1dd57cb49803079d44214a7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([24eb842](https://github.com/Ombi-app/Ombi/commit/24eb842fc4424f7bcc3ec2949d7f5472492e96f6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([ac8b16a](https://github.com/Ombi-app/Ombi/commit/ac8b16a3051ad71dbd54a8973c7dd847b564a515))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f428ce6](https://github.com/Ombi-app/Ombi/commit/f428ce6a700c081437703839bc84d2f2b1138bcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([94b16df](https://github.com/Ombi-app/Ombi/commit/94b16dfe09bf1d2cd6286777d74eb5d4496abbbb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4881775](https://github.com/Ombi-app/Ombi/commit/4881775eda69a8f136ce0d8fbbf970e3d0406dc9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8297db9](https://github.com/Ombi-app/Ombi/commit/8297db91e85da308bde6fb09ad78347dee063630))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1152ab](https://github.com/Ombi-app/Ombi/commit/d1152ab7674243daa528c524c0cdc87d81ad49c9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([eb2788b](https://github.com/Ombi-app/Ombi/commit/eb2788b761b55c487a59a049427ca08f6c10e836))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([21a794c](https://github.com/Ombi-app/Ombi/commit/21a794cbc0a5fa735ca0347c8f7f1ac04a487fbc))
## [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)
* **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))
## [4.35.7](https://github.com/Ombi-app/Ombi/compare/v4.35.6...v4.35.7) (2023-02-10)
### 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
* **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
* **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
* **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
* **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
* **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
* **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
* **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: Fixed the Request On Behalf autocomplete not filtering correctly ([a8ba2f3](https://github.com/Ombi-app/Ombi/commit/a8ba2f3544a1c01c57f217c4036a277ab0e67a09)), closes [#4539](https://github.com/Ombi-app/Ombi/issues/4539)
* **translations:** 🌐 New translations from Crowdin [skip ci] ([356c742](https://github.com/Ombi-app/Ombi/commit/356c7424e0ce8c1c5063b04bc6ed9b809f214d65))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6fcaecf](https://github.com/Ombi-app/Ombi/commit/6fcaecf80b766f2d43ac7082d74364238e1638b7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([132f4d4](https://github.com/Ombi-app/Ombi/commit/132f4d4e609b7fb7e37f38ee2f395926e2911abe))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f292006](https://github.com/Ombi-app/Ombi/commit/f292006a08894a8d0ba899c8c6e9fe863e558dda))
* **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))
## [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
* **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
* **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
* **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
* **details:** :bug: Fixed the missing Play on Media server button for 4k content [#4529](https://github.com/Ombi-app/Ombi/issues/4529) ([68600f3](https://github.com/Ombi-app/Ombi/commit/68600f3b45376e12dd2ef263d81ca4040c84cbca))
* **discover:** :bug: Fixed the issue where there was an option on the discover to request 4k shows (that's not supported currently) ([dcfd688](https://github.com/Ombi-app/Ombi/commit/dcfd688c8d2337e55fa9c6c33b7c3e80fc560cda))
* **requests:** :bug: Fixed the issue where we could no longer approve TV Requests from the requests list ([19fe4e3](https://github.com/Ombi-app/Ombi/commit/19fe4e342efe5578c26ab8ba7ee2f2e64bbc9418))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4526](https://github.com/Ombi-app/Ombi/issues/4526)) ([7e9f54f](https://github.com/Ombi-app/Ombi/commit/7e9f54fc80a09c938184e6be40ce5f49ce9673ef))
* 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))
* 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))
### 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
* **4k:** Hide 'Has 4K Request' column list if 4k feature is disabled ([#4521](https://github.com/Ombi-app/Ombi/issues/4521)) ([a9a6067](https://github.com/Ombi-app/Ombi/commit/a9a60678e74d22fa7ba34051a2645db86b600b4a))
* **issues:** Fix label ID in chatbox page ([#4520](https://github.com/Ombi-app/Ombi/issues/4520)) ([76882ad](https://github.com/Ombi-app/Ombi/commit/76882adf231f92e1cdd396239933c13467c112b3))
* **localisation:** Localize request types in notifications ([#4516](https://github.com/Ombi-app/Ombi/issues/4516)) ([e09435d](https://github.com/Ombi-app/Ombi/commit/e09435da455b12fc429f129372de31e0654da797))
* **notifications:** Remove generic admin email in favour of admins' email ([#4519](https://github.com/Ombi-app/Ombi/issues/4519)) ([b90fc5f](https://github.com/Ombi-app/Ombi/commit/b90fc5fea771a83e6cf576c71a307066efd59ea4))
* **tv:** Display TV show as requested if all episodes are requested ([#4518](https://github.com/Ombi-app/Ombi/issues/4518)) ([2ed8c48](https://github.com/Ombi-app/Ombi/commit/2ed8c48d128a69f0d144c5d332286dbf3b0bdf28))
* **plex:** :bug: Fixed not being able to enable watchlist requests in the Plex settings ([3e5158e](https://github.com/Ombi-app/Ombi/commit/3e5158ef9cda58ea2dd3be143f07aa5433691d79))
* 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))
# [4.28.0](https://github.com/Ombi-app/Ombi/compare/v4.27.8...v4.28.0) (2022-10-07)
### Features
* **email-notifications:** Add a link to Ombi details page in email notifications ([#4517](https://github.com/Ombi-app/Ombi/issues/4517)) ([a3e97b3](https://github.com/Ombi-app/Ombi/commit/a3e97b31e2298d95e7deebd71268095b8ed5e9dc))
* **media-details:** Add Trakt to social icons ([#4522](https://github.com/Ombi-app/Ombi/issues/4522)) ([d6ae79c](https://github.com/Ombi-app/Ombi/commit/d6ae79ce9eddbd5b7b888ab1b9f7e342d9d9ff9e))
* **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))
## [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
* **emby/jellyfin:** :bug: Fixed another issue where we were not correctly displaying the correct status' for movies ([5c0556e](https://github.com/Ombi-app/Ombi/commit/5c0556e6f44b8997a611f3a4d8e9e4e05d08bd13))
* **mediaserver:** fixed some more issues in the media server sync and availability checks ([f3ea979](https://github.com/Ombi-app/Ombi/commit/f3ea979b8bd77842780ce8e6928b16237dd779cf))
* **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))
## [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
* **emby:** :bug: Fixed the emby content sync [#4513](https://github.com/Ombi-app/Ombi/issues/4513) ([2927504](https://github.com/Ombi-app/Ombi/commit/2927504f0e0b4e7251e69b44e0e30c7ec9519980))
* **emby:** :bug: Fixed the emby content sync [#4513](https://github.com/Ombi-app/Ombi/issues/4513) ([bd441cb](https://github.com/Ombi-app/Ombi/commit/bd441cb54fd77d6befb03fae321dc36c29f0de2e))
* **importer:** 🐛 Allow you to only import Plex Admins without the Plex Users ([8c9ad9b](https://github.com/Ombi-app/Ombi/commit/8c9ad9b414fdc6c88bdb911d6057ae5d38783b98))
## [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
* **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
* **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
* **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
* **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
* **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
* **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>
</a>
</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">
<a href="https://github.com/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 />
<sub><b>Anojh Thayaparan</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Magikarplvl4">
<img src="https://avatars.githubusercontent.com/u/2944704?v=4" width="50;" alt="Magikarplvl4"/>
<br />
<sub><b>Magikarp Lvl 4</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Dhruv Bhavsar</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/joshuaboniface">
<img src="https://avatars.githubusercontent.com/u/4031396?v=4" width="50;" alt="joshuaboniface"/>
<br />
<sub><b>Joshua M. Boniface</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Julien Loir</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/ProtoJazz">
<img src="https://avatars.githubusercontent.com/u/1490293?v=4" width="50;" alt="ProtoJazz"/>
<br />
<sub><b>Jim MacKenize</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Taylor Buchanan</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/fservida">
<img src="https://avatars.githubusercontent.com/u/501958?v=4" width="50;" alt="fservida"/>
<br />
<sub><b>Francesco Servida</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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>
</a>
</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">
<a href="https://github.com/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>
</a>
</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">
<a href="https://github.com/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 />
<sub><b>Imgbot</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/JPyke3">
<img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="JPyke3"/>
<br />
<sub><b>Jacob Pyke</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Jono Cairns</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/krisklosterman">
<img src="https://avatars.githubusercontent.com/u/7139579?v=4" width="50;" alt="krisklosterman"/>
<br />
<sub><b>Kris Klosterman</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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>
</a>
</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">
<a href="https://github.com/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 />
<sub><b>Madeleine Schönemann</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Matt</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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>
</a>
</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">
<a href="https://github.com/beast3334">
<img src="https://avatars.githubusercontent.com/u/20631046?v=4" width="50;" alt="beast3334"/>
<br />
<sub><b>Nathan Miller</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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>
</a>
</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">
<a href="https://github.com/rob1998">
<img src="https://avatars.githubusercontent.com/u/1560707?v=4" width="50;" alt="rob1998"/>
<br />
<sub><b>Rob Gökemeijer</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Teifun2</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Tim Trott</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Xirg</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Blake Drumm</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Echel0n</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Maartenheebink</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/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 />
<sub><b>Tdorsey</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/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.ts" t="Include" />
</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.component.html" 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 Newtonsoft.Json;
using Ombi.Api.Discord.Models;
@ -12,18 +14,31 @@ namespace Ombi.Api.Discord
Api = api;
}
private const string BaseUrl = "https://discordapp.com/api/";
private const string _baseUrl = "https://discordapp.com/api/";
private IApi Api { get; }
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.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 string status { get; set; }
public bool ended { get; set; }
public string artistName { 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 Image[] images { get; set; }
public string remotePoster { get; set; }
public int qualityProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }
public string cleanName { get; set; }
public string sortName { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public Statistics statistics { get; set; }
public Addoptions addOptions { get; set; }
public string rootFolderPath { get; set; }
}
@ -49,4 +38,4 @@ namespace Ombi.Api.Lidarr.Models
None,
Unknown
}
}
}

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

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

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

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

@ -39,6 +39,7 @@ namespace Ombi.Api.Plex.Models
public string grandparentTheme { get; set; }
public string chapterSource { 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>();
}

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

@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -295,9 +296,18 @@ namespace Ombi.Api.Plex
var request = new Request("library/sections/watchlist/all", WatchlistUri, HttpMethod.Get);
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)

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Radarr.Models;
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> UpdateMovie(MovieResponse movie, 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<Tag> CreateTag(string apiKey, string baseUrl, string tagName);
}
}

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

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

@ -72,7 +72,7 @@ namespace Ombi.Api.Radarr
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);
@ -86,7 +86,8 @@ namespace Ombi.Api.Radarr
monitored = true,
year = year,
minimumAvailability = minimumAvailability,
sizeOnDisk = 0
sizeOnDisk = 0,
tags = tags.Any() ? tags.ToArray() : Enumerable.Empty<int>().ToArray()
};
if (searchNow)
@ -156,5 +157,14 @@ namespace Ombi.Api.Radarr
{
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.Threading.Tasks;
using Ombi.Api.Sonarr.Models;
using System.Net.Http;
using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr
@ -9,5 +8,8 @@ namespace Ombi.Api.Sonarr
public interface ISonarrV3Api : ISonarrApi
{
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(Episode ep)
@ -53,7 +53,7 @@ namespace Ombi.Api.Sonarr.Models
{
public Episodefile()
{
}
public Episodefile(Episodefile e)
@ -85,7 +85,7 @@ namespace Ombi.Api.Sonarr.Models
{
public EpisodeQuality()
{
}
public EpisodeQuality(EpisodeQuality e)
@ -101,7 +101,7 @@ namespace Ombi.Api.Sonarr.Models
{
public Revision()
{
}
public Revision(Revision r)
@ -113,6 +113,17 @@ namespace Ombi.Api.Sonarr.Models
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
{

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

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

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

@ -11,7 +11,6 @@ namespace Ombi.Api.Sonarr
{
public SonarrV3Api(IApi api) : base(api)
{
}
protected override string ApiBaseUrl => "/api/v3/";
@ -30,5 +29,30 @@ namespace Ombi.Api.Sonarr
request.AddHeader("X-Api-Key", apiKey);
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>
<ItemGroup>
<PackageReference Include="Twilio" Version="5.37.2" />
<PackageReference Include="Twilio" Version="5.80.1" />
</ItemGroup>
</Project>

@ -19,8 +19,8 @@ namespace Ombi.Api.Webhook
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))
{
request.AddHeader("Access-Token", accessToken);

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

@ -2,7 +2,6 @@
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers;
using Ombi.Core.Models.Requests;
@ -52,7 +51,7 @@ namespace Ombi.Core.Tests.Engine
_subject = _mocker.CreateInstance<MovieRequestEngine>();
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]

@ -3,7 +3,6 @@ using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers;
using Ombi.Core.Models;
using Ombi.Core.Services;
@ -64,7 +63,7 @@ namespace Ombi.Core.Tests.Engine
var user = new OmbiUser();
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>();
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
};
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);
@ -131,7 +130,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -206,7 +205,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -239,7 +238,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -272,7 +271,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -311,7 +310,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -344,7 +343,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -376,7 +375,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -415,7 +414,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -448,7 +447,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -481,7 +480,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -521,7 +520,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);

@ -3,7 +3,6 @@ using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers;
using Ombi.Core.Models;
using Ombi.Core.Services;
@ -62,7 +61,7 @@ namespace Ombi.Core.Tests.Engine
var user = new OmbiUser();
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>();
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
};
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);
@ -129,7 +128,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -204,7 +203,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -237,7 +236,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -270,7 +269,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -309,7 +308,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -342,7 +341,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -374,7 +373,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -413,7 +412,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -445,7 +444,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -478,7 +477,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -518,7 +517,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);

@ -3,7 +3,6 @@ using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Helpers;
using Ombi.Core.Models;
using Ombi.Core.Services;
@ -59,7 +58,7 @@ namespace Ombi.Core.Tests.Engine
var user = new OmbiUser();
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>();
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
};
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);
@ -126,7 +125,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -209,7 +208,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -243,7 +242,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -276,7 +275,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -317,7 +316,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -358,7 +357,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -380,7 +379,7 @@ namespace Ombi.Core.Tests.Engine
EpisodeRequestLimitType = RequestLimitType.Week,
Id = "id1"
};
var lastWeek = DateTime.Now.AddDays(-8);
var lastWeek = DateTime.UtcNow.AddDays(-8);
var log = new List<RequestLog>
{
new RequestLog
@ -392,7 +391,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -425,7 +424,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -466,7 +465,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -507,7 +506,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -541,7 +540,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -574,7 +573,7 @@ namespace Ombi.Core.Tests.Engine
}
};
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);
@ -615,7 +614,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);
@ -656,7 +655,7 @@ namespace Ombi.Core.Tests.Engine
},
};
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);

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

@ -8,18 +8,18 @@
</PropertyGroup>
<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="Moq" Version="4.15.1" />
<PackageReference Include="Moq.AutoMock" Version="3.0.0" />
<PackageReference Include="Nunit" Version="3.12.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.11.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Moq.AutoMock" Version="3.4.0" />
<PackageReference Include="Nunit" Version="3.13.3" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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>

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

@ -7,10 +7,8 @@ using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ombi.Core.Tests.Rule.Request
@ -31,7 +29,7 @@ namespace Ombi.Core.Tests.Rule.Request
[Test]
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
{
SeasonRequests = new List<SeasonRequests>
@ -203,7 +201,7 @@ namespace Ombi.Core.Tests.Rule.Request
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
{
@ -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.Requests;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ombi.Core.Tests.Rule.Request
@ -29,7 +27,7 @@ namespace Ombi.Core.Tests.Rule.Request
[Test]
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
{
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",
Email = "Test@test.com"
}
}.AsQueryable().BuildMock().Object);
}.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model);
@ -95,7 +95,7 @@ namespace Ombi.Core.Tests.Senders
Id = "b",
Email = "b@test.com"
}
}.AsQueryable().BuildMock().Object);
}.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model);
@ -129,7 +129,7 @@ namespace Ombi.Core.Tests.Senders
{
Id = "a",
}
}.AsQueryable().BuildMock().Object);
}.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model);
_mocker.Verify<ILogger<MassEmailSender>>(
@ -177,7 +177,7 @@ namespace Ombi.Core.Tests.Senders
Id = "b",
Email = "b@test.com"
}
}.AsQueryable().BuildMock().Object);
}.AsQueryable().BuildMock());
var result = await _subject.SendMassEmail(model);
@ -217,7 +217,7 @@ namespace Ombi.Core.Tests.Senders
{
Id = "b",
}
}.AsQueryable().BuildMock().Object);
}.AsQueryable().BuildMock());
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)
{
existingRequest.Is4kRequest = true;
existingRequest.RequestedDate4k = DateTime.Now;
existingRequest.RequestedDate4k = DateTime.UtcNow;
}
else
{
existingRequest.RequestedDate = DateTime.Now;
existingRequest.RequestedDate = DateTime.UtcNow;
}
isExisting = true;
requestModel = existingRequest;
@ -134,7 +134,7 @@ namespace Ombi.Core.Engine
? DateTime.Parse(movieInfo.ReleaseDate)
: DateTime.MinValue,
Status = movieInfo.Status,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.Now,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.UtcNow,
Approved = false,
Approved4K = false,
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
@ -143,7 +143,7 @@ namespace Ombi.Core.Engine
RequestedByAlias = model.RequestedByAlias,
RootPathOverride = model.RootFolderOverride.GetValueOrDefault(),
QualityOverride = model.QualityPathOverride.GetValueOrDefault(),
RequestedDate4k = model.Is4kRequest ? DateTime.Now : DateTime.MinValue,
RequestedDate4k = model.Is4kRequest ? DateTime.UtcNow : DateTime.MinValue,
Is4kRequest = model.Is4kRequest,
Source = model.Source
};

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

@ -25,28 +25,29 @@ namespace Ombi.Core.Engine
{
// get all movie requests
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 children = tv.SelectMany(x =>
x.ChildRequests.Where(c => c.RequestedDate >= request.From && c.RequestedDate <= request.To));
var children = await tv.SelectMany(x =>
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 childrenCount = children.CountAsync();
var moviesCount = filteredMovies.Count;
var childrenCount = children.Count;
var availableMovies =
filteredMovies.Select(x => x.MarkedAsAvailable >= request.From && x.MarkedAsAvailable <= request.To).CountAsync();
var availableChildren = children.Where(c => c.MarkedAsAvailable >= request.From && c.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).Count();
return new UserStatsSummary
{
TotalMovieRequests = await moviesCount,
TotalTvRequests = await childrenCount,
CompletedRequestsTv = await availableChildren,
CompletedRequestsMovies = await availableMovies,
MostRequestedUserMovie = (await userMovie).FirstOrDefault()?.RequestedUser ?? new OmbiUser(),
MostRequestedUserTv = (await userTv).FirstOrDefault()?.RequestedUser ?? new OmbiUser(),
TotalMovieRequests = moviesCount,
TotalTvRequests = childrenCount,
CompletedRequestsTv = availableChildren,
CompletedRequestsMovies = availableMovies,
MostRequestedUserMovie = userMovie.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
{
public interface IImageService
{
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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Api.FanartTv;
using Ombi.Api.TheMovieDb;
using Ombi.Core.Helpers;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Repository;
namespace Ombi.Core
@ -12,13 +17,19 @@ namespace Ombi.Core
private readonly IApplicationConfigRepository _configRepository;
private readonly IFanartTvApi _fanartTvApi;
private readonly ICacheService _cache;
private readonly IMovieDbApi _movieDbApi;
private readonly ICurrentUser _user;
private readonly ISettingsService<OmbiSettings> _ombiSettings;
public ImageService(IApplicationConfigRepository configRepository, IFanartTvApi fanartTvApi,
ICacheService cache)
ICacheService cache, IMovieDbApi movieDbApi, ICurrentUser user, ISettingsService<OmbiSettings> ombiSettings)
{
_configRepository = configRepository;
_fanartTvApi = fanartTvApi;
_cache = cache;
_movieDbApi = movieDbApi;
_user = user;
_ombiSettings = ombiSettings;
}
public async Task<string> GetTvBackground(string tvdbId)
@ -43,5 +54,69 @@ namespace Ombi.Core
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 bool IsValid { get; set; }
public string Version { get; set; }
public string ExpectedSubDir { get; set; }
}
}

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

@ -15,6 +15,8 @@ using Ombi.Store.Entities;
using Ombi.Store.Repository;
using System.Collections.Generic;
using Ombi.Api.Radarr.Models;
using Microsoft.Extensions.Options;
using Ombi.Api.Sonarr;
namespace Ombi.Core.Senders
{
@ -67,7 +69,7 @@ namespace Ombi.Core.Senders
}
if (radarrSettings.Enabled)
{
return await SendToRadarr(model, is4K, radarrSettings);
return await SendToRadarr(model, radarrSettings);
}
var dogSettings = await _dogNzbSettings.GetSettingsAsync();
@ -131,7 +133,7 @@ namespace Ombi.Core.Senders
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);
@ -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
if (model.QualityOverride > 0)
{
@ -174,7 +187,7 @@ namespace Ombi.Core.Senders
{
var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability);
settings.MinimumAvailability, tags);
if (!string.IsNullOrEmpty(result.Error?.message))
{
@ -212,5 +225,17 @@ namespace Ombi.Core.Senders
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
using Ombi.Api.DogNzb;
using Ombi.Api.DogNzb.Models;
using Ombi.Api.SickRage;
@ -25,8 +27,7 @@ namespace Ombi.Core.Senders
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
SonarrApi = sonarrApi;
SonarrV3Api = sonarrV3Api;
SonarrApi = sonarrV3Api;
Logger = log;
SonarrSettings = sonarrSettings;
DogNzbSettings = dog;
@ -38,8 +39,7 @@ namespace Ombi.Core.Senders
_notificationHelper = notify;
}
private ISonarrApi SonarrApi { get; }
private ISonarrV3Api SonarrV3Api { get; }
private ISonarrV3Api SonarrApi { get; }
private IDogNzbApi DogNzbApi { get; }
private ISickRageApi SickRageApi { get; }
private ILogger<TvSender> Logger { get; }
@ -155,12 +155,13 @@ namespace Ombi.Core.Senders
{
return null;
}
var options = new SonarrSendOptions();
int qualityToUse;
var sonarrV3 = s.V3;
var languageProfileId = s.LanguageProfile;
string rootFolderPath;
string seriesType;
int? tagToUse = null;
var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
@ -191,6 +192,7 @@ namespace Ombi.Core.Senders
}
}
seriesType = "anime";
tagToUse = s.AnimeTag;
}
else
{
@ -210,6 +212,7 @@ namespace Ombi.Core.Senders
}
}
seriesType = "standard";
tagToUse = s.Tag;
}
// Overrides on the request take priority
@ -241,6 +244,16 @@ namespace Ombi.Core.Senders
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?
var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri);
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
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.
}
},
languageProfileId = languageProfileId,
tags = options.Tags
};
if (sonarrV3)
{
newSeries.languageProfileId = languageProfileId;
}
// Montitor the correct seasons,
// 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));
}
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
await SendToSonarr(model, existingSeries, s);
await SendToSonarr(model, existingSeries, s, options);
}
else
{
await SendToSonarr(model, existingSeries, s);
await SendToSonarr(model, existingSeries, s, options);
}
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
Season existingSeason = null;
@ -324,15 +358,27 @@ namespace Ombi.Core.Senders
}
}
var episodesToUpdate = new List<Episode>();
// Ok, now let's sort out the episodes.
// Does the show have the correct tags we are expecting
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)
{
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 sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>();
while (!sonarrEpList.Any())
@ -376,16 +422,10 @@ namespace Ombi.Core.Senders
epToUnmonitored.Add(ep);
}
foreach (var epToUpdate in epToUnmonitored)
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
await SonarrApi.MonitorEpisode(epToUnmonitored.Select(x => x.id).ToArray(), false, s.ApiKey, s.FullUri);
}
// Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber))
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
await SonarrApi.MonitorEpisode(episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber).Select(x => x.id).ToArray(), true, s.ApiKey, s.FullUri);
}
if (!s.AddOnly)
@ -527,7 +567,7 @@ namespace Ombi.Core.Senders
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;
}

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

@ -11,7 +11,7 @@
</PropertyGroup>
<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.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />

@ -6,7 +6,7 @@
</PropertyGroup>
<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="System.Collections" Version="4.3.0" />
</ItemGroup>

@ -9,10 +9,10 @@
</PropertyGroup>
<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="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
</ItemGroup>
<ItemGroup>

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

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

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

@ -11,15 +11,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.1.3" />
<PackageReference Include="EasyCrypto" Version="4.5.0" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.0" />
<PackageReference Include="Quartz" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Quartz" Version="3.5.0" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
</ItemGroup>
</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.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
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 = 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;
_userManager = 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();
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 _);
return base.OnDisconnectedAsync(exception);
}
public Task Notification(string data)
{
return Clients.All.SendAsync(NotificationEvent, data);
}
UserId = userIdClaim.Value,
Roles = claims
});
await base.OnConnectedAsync();
}
public class HubUsers
public override async Task OnDisconnectedAsync(Exception exception)
{
public string UserId { get; set; }
public IList<string> Roles { get; set; } = new List<string>();
UsersOnline.TryRemove(Context.ConnectionId, out _);
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>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.SignalR" Version="2.4.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</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>
</resheader>
<data name="NewAlbums" xml:space="preserve">
<value>New Albums</value>
<value>Nový album</value>
</data>
<data name="NewMovies" xml:space="preserve">
<value>New Movies</value>
<value>Nové filmy</value>
</data>
<data name="NewTV" xml:space="preserve">
<value>New TV</value>
<value>Nové seriály</value>
</data>
<data name="GenresLabel" xml:space="preserve">
<value>Žánre:</value>
@ -139,18 +139,18 @@
<value>Epizódy:</value>
</data>
<data name="PoweredBy" xml:space="preserve">
<value>Powered by</value>
<value>Beží na</value>
</data>
<data name="Unsubscribe" xml:space="preserve">
<value>Unsubscribe</value>
<value>Zrušiť odber</value>
</data>
<data name="Album" xml:space="preserve">
<value>Album</value>
</data>
<data name="Movie" xml:space="preserve">
<value>Movie</value>
<value>Film</value>
</data>
<data name="TvShow" xml:space="preserve">
<value>TV Show</value>
<value>Seriál</value>
</data>
</root>

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

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

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

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

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

@ -186,6 +186,14 @@ namespace Ombi.Notifications
{
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 PartiallyAvailableEpisodeNumbers { get; set; }
public string PartiallyAvailableSeasonNumber { get; set; }
public string PartiallyAvailableEpisodeCount { get; set; }
public string PartiallyAvailableEpisodesList { get; set; }
// System Defined
private string LongDate => DateTime.Now.ToString("D");
@ -336,6 +346,8 @@ namespace Ombi.Notifications
{ nameof(ProviderId), ProviderId },
{ nameof(PartiallyAvailableEpisodeNumbers), PartiallyAvailableEpisodeNumbers },
{ nameof(PartiallyAvailableSeasonNumber), PartiallyAvailableSeasonNumber },
{ nameof(PartiallyAvailableEpisodesList), PartiallyAvailableEpisodesList },
{ nameof(PartiallyAvailableEpisodeCount), PartiallyAvailableEpisodeCount },
};
}
}

@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ensure.That" Version="7.0.0-pre32" />
<PackageReference Include="MailKit" Version="2.5.0" />
<PackageReference Include="Ensure.That" Version="10.1.0" />
<PackageReference Include="MailKit" Version="3.4.1" />
</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.Repository;
using System.Threading.Tasks;
using MockQueryable;
using MockQueryable.Moq;
namespace Ombi.Schedule.Tests
@ -51,7 +50,7 @@ namespace Ombi.Schedule.Tests
};
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);
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 });
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);
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 });
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);
Assert.That(issues[0].Status, Is.Not.EqualTo(IssueStatus.Deleted));

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

@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Castle.Components.DictionaryAdapter;
using Microsoft.AspNetCore.SignalR;
using Moq;
using MockQueryable.Moq;
using NUnit.Framework;
using Ombi.Core;
using Ombi.Core.Notifications;
using Ombi.Hubs;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Store.Entities;
@ -19,49 +14,140 @@ using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
using Ombi.Helpers;
using Ombi.Core.Services;
using Ombi.Tests;
using Moq.AutoMock;
using Ombi.Settings.Settings.Models;
using Ombi.Notifications.Models;
namespace Ombi.Schedule.Tests
{
[TestFixture]
[Ignore("Need to work out how to mockout the hub context")]
public class PlexAvailabilityCheckerTests
{
private AutoMocker _mocker;
private PlexAvailabilityChecker _subject;
[SetUp]
public void Setup()
{
_repo = new Mock<IPlexContentRepository>();
_tv = new Mock<ITvRequestRepository>();
_movie = new Mock<IMovieRequestRepository>();
_notify = new Mock<INotificationHelper>();
var hub = new Mock<IHubContext<NotificationHub>>();
hub.Setup(x =>
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>());
_mocker = new AutoMocker();
var hub = SignalRHelper.MockHub<NotificationHub>();
_mocker.Use(hub);
_subject = _mocker.CreateInstance<PlexAvailabilityChecker>();
}
[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;
private Mock<ITvRequestRepository> _tv;
private Mock<IMovieRequestRepository> _movie;
private Mock<INotificationHelper> _notify;
private PlexAvailabilityChecker Checker;
_mocker.Verify<IMovieRequestRepository>(x => x.SaveChangesAsync(), Times.Once);
_mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.ImdbId), Times.Never);
_mocker.Verify<IPlexContentRepository>(x => x.Get(It.IsAny<string>(), ProviderType.TheMovieDbId), Times.Once);
_mocker.Verify<INotificationHelper>(x => x.Notify(It.Is<NotificationOptions>(x => x.NotificationType == NotificationType.RequestAvailable)), Times.Once);
}
[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
{
ImdbId = "test"
};
_movie.Setup(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
_repo.Setup(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent());
_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 { 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]
@ -71,19 +157,96 @@ namespace Ombi.Schedule.Tests
{
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);
}
[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>
{
new SeasonRequests
@ -93,7 +256,7 @@ namespace Ombi.Schedule.Tests
new EpisodeRequests
{
EpisodeNumber = 1,
Season = new SeasonRequests
Season = new SeasonRequests
{
SeasonNumber = 2
}
@ -106,27 +269,6 @@ namespace Ombi.Schedule.Tests
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.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
@ -48,7 +46,7 @@ namespace Ombi.Schedule.Tests
};
var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexContentRepository>(x =>
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x =>
x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>()))
.Returns(Task.FromResult(new PlexServerContent()));
@ -108,7 +106,7 @@ namespace Ombi.Schedule.Tests
};
var contentToAdd = new HashSet<PlexServerContent>();
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
{
MediaContainer = new Mediacontainer
@ -166,7 +164,7 @@ namespace Ombi.Schedule.Tests
};
var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexContentRepository>(x =>
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x =>
x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>()))
.Returns(Task.FromResult(new PlexServerContent
{
@ -204,7 +202,7 @@ namespace Ombi.Schedule.Tests
};
var contentToAdd = new HashSet<PlexServerContent>();
var contentProcessed = new Dictionary<int, string>();
_mocker.Setup<IPlexContentRepository>(x =>
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x =>
x.GetFirstContentByCustom(It.IsAny<Expression<Func<PlexServerContent, bool>>>()))
.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 NUnit.Framework;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.Requests;
@ -32,11 +35,12 @@ namespace Ombi.Schedule.Tests
public void Setup()
{
_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);
_context = _mocker.GetMock<IJobExecutionContext>();
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
}
[Test]
@ -53,6 +57,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task TerminatesWhenWatchlistIsNotEnabled()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false });
await _subject.Execute(null);
_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);
}
[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]
public async Task NoPlexUsersWithToken()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
var um = MockHelper.MockUserManager(new List<OmbiUser>
{
@ -102,6 +172,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task MultipleUsers()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
var um = MockHelper.MockUserManager(new List<OmbiUser>
{
@ -125,6 +196,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task MovieRequestFromWatchList_NoGuid()
{
_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
{
@ -175,6 +247,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task TvRequestFromWatchList_NoGuid()
{
_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
{
@ -224,6 +297,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task MovieRequestFromWatchList_AlreadyRequested()
{
_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
{
@ -322,6 +396,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task MovieRequestFromWatchList_NoTmdbGuid()
{
_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
{
@ -360,6 +435,7 @@ namespace Ombi.Schedule.Tests
});
_mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_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);
@ -368,9 +444,195 @@ namespace Ombi.Schedule.Tests
_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]
public async Task TvRequestFromWatchList_NoTmdbGuid()
{
_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
{
@ -420,6 +682,7 @@ namespace Ombi.Schedule.Tests
[Test]
public async Task MovieRequestFromWatchList_AlreadyImported()
{
_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
{
@ -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.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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core;
@ -11,7 +10,6 @@ using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Plex.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -20,37 +18,30 @@ using Quartz;
namespace Ombi.Schedule.Jobs.Radarr
{
public class ArrAvailabilityChecker : IArrAvailabilityChecker
public class ArrAvailabilityChecker : AvailabilityChecker, IArrAvailabilityChecker
{
private readonly IExternalRepository<RadarrCache> _radarrRepo;
private readonly IExternalRepository<SonarrCache> _sonarrRepo;
private readonly ILogger<ArrAvailabilityChecker> _logger;
private readonly ISettingsService<RadarrSettings> _radarrSettings;
private readonly ISettingsService<SonarrSettings> _sonarrSettings;
private readonly IExternalRepository<SonarrEpisodeCache> _sonarrEpisodeRepo;
private readonly INotificationHelper _notification;
private readonly IHubContext<NotificationHub> _hub;
private readonly ITvRequestRepository _tvRequest;
private readonly IMovieRequestRepository _movies;
public ArrAvailabilityChecker(
IExternalRepository<RadarrCache> radarrRepo,
IExternalRepository<SonarrCache> sonarrRepo,
IExternalRepository<SonarrEpisodeCache> sonarrEpisodeRepo,
INotificationHelper notification, IHubContext<NotificationHub> hub,
INotificationHelper notification, INotificationHubService notificationHubService,
ITvRequestRepository tvRequest, IMovieRequestRepository movies,
ILogger<ArrAvailabilityChecker> log,
ISettingsService<RadarrSettings> radarrSettings,
ISettingsService<SonarrSettings> sonarrSettings)
: base(tvRequest, notification, log, notificationHubService)
{
_radarrRepo = radarrRepo;
_sonarrRepo = sonarrRepo;
_sonarrEpisodeRepo = sonarrEpisodeRepo;
_notification = notification;
_hub = hub;
_tvRequest = tvRequest;
_movies = movies;
_logger = log;
_radarrSettings = radarrSettings;
_sonarrSettings = sonarrSettings;
}
@ -82,7 +73,7 @@ namespace Ombi.Schedule.Jobs.Radarr
var available = availableRadarrMovies.FirstOrDefault(x => x.TheMovieDbId == movieRequest.TheMovieDbId);
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)
{
itemsForAvailability.Add(new AvailabilityModel
@ -109,12 +100,12 @@ namespace Ombi.Schedule.Jobs.Radarr
if (itemsForAvailability.Any())
{
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Radarr Availability Checker found some new available movies!");
await NotificationHubService.SendNotificationToAdmins("Radarr Availability Checker found some new available movies!");
}
foreach (var item in itemsForAvailability)
{
await _notification.Notify(new NotificationOptions
await _notificationService.Notify(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
@ -127,9 +118,9 @@ namespace Ombi.Schedule.Jobs.Radarr
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);
foreach (var child in tv)
{
var tvDbId = child.ParentRequest.TvDbId;
@ -140,83 +131,10 @@ namespace Ombi.Schedule.Jobs.Radarr
continue;
}
//if (!seriesEpisodes.Any())
//{
// // 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 ProcessTvShow(seriesEpisodes, child);
}
await _tvRequest.Save();
await _tvRepo.Save();
}
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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.CouchPotato;
@ -45,13 +44,13 @@ namespace Ombi.Schedule.Jobs.Couchpotato
public class CouchPotatoSync : ICouchPotatoSync
{
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;
_api = api;
_log = log;
_ctx = ctx;
_notification = hub;
_notification = notificationHubService;
_settings.ClearCache();
}
@ -59,7 +58,7 @@ namespace Ombi.Schedule.Jobs.Couchpotato
private readonly ICouchPotatoApi _api;
private readonly ILogger<CouchPotatoSync> _log;
private readonly ExternalContext _ctx;
private readonly IHubContext<NotificationHub> _notification;
private readonly INotificationHubService _notification;
public async Task Execute(IJobExecutionContext job)
{
@ -69,8 +68,7 @@ namespace Ombi.Schedule.Jobs.Couchpotato
return;
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Started");
await _notification.SendNotificationToAdmins("Couch Potato Sync Started");
try
{
_log.LogInformation(LoggingEvents.CouchPotatoCacher, "Getting all active movies from CP");
@ -81,11 +79,9 @@ namespace Ombi.Schedule.Jobs.Couchpotato
await strat.ExecuteAsync(async () =>
{
// Let's remove the old cached data
using (var tran = await _ctx.Database.BeginTransactionAsync())
{
await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache");
tran.Commit();
}
using var tran = await _ctx.Database.BeginTransactionAsync();
await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache");
await tran.CommitAsync();
});
// Save
@ -109,23 +105,19 @@ namespace Ombi.Schedule.Jobs.Couchpotato
strat = _ctx.Database.CreateExecutionStrategy();
await strat.ExecuteAsync(async () =>
{
using (var tran = await _ctx.Database.BeginTransactionAsync())
{
await _ctx.CouchPotatoCache.AddRangeAsync(movieIds);
using var tran = await _ctx.Database.BeginTransactionAsync();
await _ctx.CouchPotatoCache.AddRangeAsync(movieIds);
await _ctx.SaveChangesAsync();
tran.Commit();
}
await _ctx.SaveChangesAsync();
await tran.CommitAsync();
});
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Finished");
await _notification.SendNotificationToAdmins("Couch Potato Sync Finished");
}
}
catch (Exception e)
{
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Failed");
await _notification.SendNotificationToAdmins("Couch Potato Sync Failed");
_log.LogError(LoggingEvents.CouchPotatoCacher, e, "error when trying to get movies from CP");
throw;
}

@ -1,17 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core;
using Ombi.Core.Notifications;
using Ombi.Core.Services;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -20,39 +16,30 @@ using Quartz;
namespace Ombi.Schedule.Jobs.Emby
{
public class EmbyAvaliabilityChecker : IEmbyAvaliabilityChecker
public class EmbyAvaliabilityChecker : AvailabilityChecker, IEmbyAvaliabilityChecker
{
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;
_tvRepo = t;
_movieRepo = m;
_notificationService = n;
_log = log;
_notification = notification;
_featureService = featureService;
}
private readonly ITvRequestRepository _tvRepo;
private readonly IMovieRequestRepository _movieRepo;
private readonly IEmbyContentRepository _repo;
private readonly INotificationHelper _notificationService;
private readonly ILogger<EmbyAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _notification;
private readonly IFeatureService _featureService;
public async Task Execute(IJobExecutionContext job)
{
_log.LogInformation("Starting Emby Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Emby Availability Checker Started");
await NotificationHubService.SendNotificationToAdmins("Emby Availability Checker Started");
await ProcessMovies();
await ProcessTv();
_log.LogInformation("Finished Emby Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Emby Availability Checker Finished");
await NotificationHubService.SendNotificationToAdmins("Emby Availability Checker Finished");
}
private async Task ProcessMovies()
@ -167,68 +154,7 @@ namespace Ombi.Schedule.Jobs.Emby
x.Series.Title == child.Title);
}
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.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 ProcessTvShow(seriesEpisodes, child);
}
await _tvRepo.Save();

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

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

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

@ -26,10 +26,8 @@
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core;
@ -45,39 +43,30 @@ using Quartz;
namespace Ombi.Schedule.Jobs.Jellyfin
{
public class JellyfinAvaliabilityChecker : IJellyfinAvaliabilityChecker
public class JellyfinAvaliabilityChecker : AvailabilityChecker, IJellyfinAvaliabilityChecker
{
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;
_tvRepo = t;
_movieRepo = m;
_notificationService = n;
_log = log;
_notification = notification;
_featureService = featureService;
}
private readonly ITvRequestRepository _tvRepo;
private readonly IMovieRequestRepository _movieRepo;
private readonly IJellyfinContentRepository _repo;
private readonly INotificationHelper _notificationService;
private readonly ILogger<JellyfinAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _notification;
private readonly IFeatureService _featureService;
public async Task Execute(IJobExecutionContext job)
{
_log.LogInformation("Starting Jellyfin Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Started");
await NotificationHubService.SendNotificationToAdmins("Jellyfin Availability Checker Started");
await ProcessMovies();
await ProcessTv();
_log.LogInformation("Finished Jellyfin Availability Check");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Finished");
await NotificationHubService.SendNotificationToAdmins("Jellyfin Availability Checker Finished");
}
private async Task ProcessMovies()
@ -193,68 +182,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
x.Series.Title == child.Title);
}
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.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 ProcessTvShow(seriesEpisodes, child);
}
await _tvRepo.Save();

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

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

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

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

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