mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-22 14:13:36 -07:00
Merge branch 'Ombi-app:develop' into develop
This commit is contained in:
commit
fc7d087a8d
159 changed files with 9605 additions and 4384 deletions
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
exitOnceUploaded: true
|
||||
|
||||
- name: Publish to Chromatic and auto accept changes
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
uses: chromaui/action@v1
|
||||
with:
|
||||
projectToken: 7c47e1a1a4bd
|
||||
|
|
30
.github/workflows/cypress.yml
vendored
30
.github/workflows/cypress.yml
vendored
|
@ -34,16 +34,28 @@ jobs:
|
|||
- name: Install Frontend Deps
|
||||
run: yarn --cwd ./src/Ombi/ClientApp install
|
||||
|
||||
- name: Start Frontend
|
||||
run: |
|
||||
nohup yarn --cwd ./src/Ombi/ClientApp start &
|
||||
- name: Build Frontend
|
||||
run: yarn --cwd ./src/Ombi/ClientApp build
|
||||
|
||||
- name: Install Automation Deps
|
||||
run: yarn --cwd ./tests install
|
||||
- name: Build Docker Image
|
||||
run: docker build -t ombi src/
|
||||
|
||||
- name: Start Backend
|
||||
run: |
|
||||
nohup dotnet run --project ./src/Ombi -- --host http://*:3577 &
|
||||
- name: Run Docker Image
|
||||
run: nohup docker run --rm -p 5000:5000 ombi &
|
||||
|
||||
- name: Sleep for server to start
|
||||
run: sleep 20
|
||||
|
||||
# - name: Start Frontend
|
||||
# run: |
|
||||
# nohup yarn --cwd ./src/Ombi/ClientApp start &
|
||||
|
||||
# - name: Install Automation Deps
|
||||
# run: yarn --cwd ./tests install
|
||||
|
||||
# - name: Start Backend
|
||||
# run: |
|
||||
# nohup dotnet run --project ./src/Ombi -- --host http://*:3577 &
|
||||
|
||||
- name: Cypress Tests
|
||||
uses: cypress-io/github-action@v2.8.2
|
||||
|
@ -52,7 +64,7 @@ jobs:
|
|||
browser: chrome
|
||||
headless: true
|
||||
working-directory: tests
|
||||
wait-on: http://localhost:3577/
|
||||
wait-on: http://localhost:5000/
|
||||
# 10 minutes
|
||||
wait-on-timeout: 600
|
||||
env:
|
||||
|
|
237
CHANGELOG.md
237
CHANGELOG.md
|
@ -1,3 +1,145 @@
|
|||
## [4.27.6](https://github.com/Ombi-app/Ombi/compare/v4.27.5...v4.27.6) (2022-10-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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.27.5](https://github.com/Ombi-app/Ombi/compare/v4.27.4...v4.27.5) (2022-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **importer:** 🐛 Allow you to only import Plex Admins without the Plex Users ([8c9ad9b](https://github.com/Ombi-app/Ombi/commit/8c9ad9b414fdc6c88bdb911d6057ae5d38783b98))
|
||||
|
||||
|
||||
|
||||
## [4.27.4](https://github.com/Ombi-app/Ombi/compare/v4.27.3...v4.27.4) (2022-09-30)
|
||||
|
||||
|
||||
|
||||
## [4.27.3](https://github.com/Ombi-app/Ombi/compare/v4.27.2...v4.27.3) (2022-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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
|
||||
|
||||
* **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.27.1](https://github.com/Ombi-app/Ombi/compare/v4.27.0...v4.27.1) (2022-09-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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.27.0](https://github.com/Ombi-app/Ombi/compare/v4.26.0...v4.27.0) (2022-09-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Recently requested improvements ([#4755](https://github.com/Ombi-app/Ombi/issues/4755)) ([ff04d87](https://github.com/Ombi-app/Ombi/commit/ff04d875343604c77c391bf55d0968977e480281))
|
||||
|
||||
|
||||
|
||||
# [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
|
||||
|
||||
* **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.25.0](https://github.com/Ombi-app/Ombi/compare/v4.24.0...v4.25.0) (2022-08-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 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.23.2](https://github.com/Ombi-app/Ombi/compare/v4.23.1...v4.23.2) (2022-08-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix conflicting property name for Swagger ([#4733](https://github.com/Ombi-app/Ombi/issues/4733)) ([d661f32](https://github.com/Ombi-app/Ombi/commit/d661f32e8a9e105faab6380b4b7b642896b98163))
|
||||
|
||||
|
||||
|
||||
## [4.23.1](https://github.com/Ombi-app/Ombi/compare/v4.23.0...v4.23.1) (2022-08-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Localize recently requested on discover page ([#4729](https://github.com/Ombi-app/Ombi/issues/4729)) ([bf65c76](https://github.com/Ombi-app/Ombi/commit/bf65c76ff9ce38f65a9e5feb872734e8d8e35eb6))
|
||||
|
||||
|
||||
|
||||
# [4.23.0](https://github.com/Ombi-app/Ombi/compare/v4.22.4...v4.23.0) (2022-08-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Log Microsoft warnings to log file ([#4723](https://github.com/Ombi-app/Ombi/issues/4723)) ([26ac75f](https://github.com/Ombi-app/Ombi/commit/26ac75f0c223c2a91e3471797ae46ede3fde89cc))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Recently Requested on Discover Page ([#4387](https://github.com/Ombi-app/Ombi/issues/4387)) ([44d38fb](https://github.com/Ombi-app/Ombi/commit/44d38fbaae521dbb467b61c7471b2384015ac52e))
|
||||
|
||||
|
||||
|
||||
## [4.22.4](https://github.com/Ombi-app/Ombi/compare/v4.22.3...v4.22.4) (2022-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* :bug: Fixed missing externals ([#4712](https://github.com/Ombi-app/Ombi/issues/4712)) ([fcc1eaa](https://github.com/Ombi-app/Ombi/commit/fcc1eaaa377683dcdc81d62a2a688fb0c4490c7b))
|
||||
* fixed trakt image not loading when base url present ([#4711](https://github.com/Ombi-app/Ombi/issues/4711)) ([f102dcf](https://github.com/Ombi-app/Ombi/commit/f102dcf751c2eb62ebfe30f9f8e4b2ad863c3b0d))
|
||||
* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4713](https://github.com/Ombi-app/Ombi/issues/4713)) ([ff142b0](https://github.com/Ombi-app/Ombi/commit/ff142b09abbb2f9540387284222552e6e12639fe))
|
||||
|
||||
|
||||
|
||||
## [4.22.3](https://github.com/Ombi-app/Ombi/compare/v4.22.2...v4.22.3) (2022-07-28)
|
||||
|
||||
|
||||
|
@ -264,98 +406,3 @@
|
|||
|
||||
|
||||
|
||||
## [4.16.8](https://github.com/Ombi-app/Ombi/compare/v4.16.7...v4.16.8) (2022-04-13)
|
||||
|
||||
|
||||
### 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))
|
||||
|
||||
|
||||
|
||||
## [4.16.7](https://github.com/Ombi-app/Ombi/compare/v4.16.6...v4.16.7) (2022-04-12)
|
||||
|
||||
|
||||
|
||||
## [4.16.6](https://github.com/Ombi-app/Ombi/compare/v4.16.5...v4.16.6) (2022-04-11)
|
||||
|
||||
|
||||
|
||||
## [4.16.5](https://github.com/Ombi-app/Ombi/compare/v4.16.4...v4.16.5) (2022-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **watchlist:** actually fixed it this time... ([d962a32](https://github.com/Ombi-app/Ombi/commit/d962a3211eca29520662ddce962676e3aea17ec5))
|
||||
|
||||
|
||||
|
||||
## [4.16.4](https://github.com/Ombi-app/Ombi/compare/v4.16.3...v4.16.4) (2022-04-08)
|
||||
|
||||
|
||||
|
||||
## [4.16.3](https://github.com/Ombi-app/Ombi/compare/v4.16.2...v4.16.3) (2022-04-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))
|
||||
|
||||
|
||||
|
||||
## [4.16.2](https://github.com/Ombi-app/Ombi/compare/v4.16.1...v4.16.2) (2022-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **wizard:** Fixed an issue when using Plex OAuth it could fail setting up ([b743cf4](https://github.com/Ombi-app/Ombi/commit/b743cf4fafa7341ad1b163276f006d7ab0e9dcff))
|
||||
|
||||
|
||||
|
||||
## [4.16.1](https://github.com/Ombi-app/Ombi/compare/v4.16.0...v4.16.1) (2022-04-07)
|
||||
|
||||
|
||||
|
||||
# [4.16.0](https://github.com/Ombi-app/Ombi/compare/v4.15.6...v4.16.0) (2022-04-07)
|
||||
|
||||
|
||||
|
||||
## [4.15.6](https://github.com/Ombi-app/Ombi/compare/v4.15.5...v4.15.6) (2022-04-07)
|
||||
|
||||
|
||||
### 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))
|
||||
|
||||
|
||||
|
||||
## [4.15.5](https://github.com/Ombi-app/Ombi/compare/v4.15.4...v4.15.5) (2022-04-06)
|
||||
|
||||
|
||||
|
||||
## [4.15.4](https://github.com/Ombi-app/Ombi/compare/v4.15.3...v4.15.4) (2022-03-29)
|
||||
|
||||
|
||||
|
||||
## [4.15.3](https://github.com/Ombi-app/Ombi/compare/v4.15.2...v4.15.3) (2022-03-24)
|
||||
|
||||
|
||||
|
||||
## [4.15.2](https://github.com/Ombi-app/Ombi/compare/v4.15.1...v4.15.2) (2022-03-23)
|
||||
|
||||
|
||||
### 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.15.1](https://github.com/Ombi-app/Ombi/compare/v4.15.0...v4.15.1) (2022-03-18)
|
||||
|
||||
|
||||
### 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))
|
||||
|
||||
|
||||
|
||||
|
|
66
README.md
66
README.md
|
@ -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"/>
|
||||
|
@ -621,15 +628,15 @@ Here are some of the features Ombi has:
|
|||
<br />
|
||||
<sub><b>Marley</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mattmattmatt">
|
||||
<img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/>
|
||||
<br />
|
||||
<sub><b>Matt</b></sub>
|
||||
</a>
|
||||
</td></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,6 +651,13 @@ 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"/>
|
||||
|
@ -657,7 +671,8 @@ Here are some of the features Ombi has:
|
|||
<br />
|
||||
<sub><b>Qiming Chen</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/randallbruder">
|
||||
<img src="https://avatars.githubusercontent.com/u/6447487?v=4" width="50;" alt="randallbruder"/>
|
||||
|
@ -671,8 +686,7 @@ Here are some of the features Ombi has:
|
|||
<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"/>
|
||||
|
@ -809,6 +823,13 @@ Here are some of the features Ombi has:
|
|||
<sub><b>Patrick Weber</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mkgeeky">
|
||||
<img src="https://avatars.githubusercontent.com/u/68811367?v=4" width="50;" alt="mkgeeky"/>
|
||||
<br />
|
||||
<sub><b>Mkgeeky</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/sir-marv">
|
||||
<img src="https://avatars.githubusercontent.com/u/3598205?v=4" width="50;" alt="sir-marv"/>
|
||||
|
@ -822,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"/>
|
||||
|
|
285
src/.dockerignore
Normal file
285
src/.dockerignore
Normal file
|
@ -0,0 +1,285 @@
|
|||
**/bin/
|
||||
**/obj/
|
||||
**/.angular/
|
||||
**/node_modules/
|
||||
.gitignore
|
||||
.git/
|
||||
|
||||
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
Properties/launchSettings.json
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/
|
||||
tools/Cake.CoreCLR
|
||||
.vscode
|
||||
tools
|
||||
.dotnet
|
||||
Dockerfile
|
||||
|
||||
# .env file contains default environment variables for docker
|
||||
.env
|
||||
.git/
|
6
src/.idea/.idea.Ombi/.idea/contentModel.xml
generated
6
src/.idea/.idea.Ombi/.idea/contentModel.xml
generated
|
@ -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;
|
||||
|
@ -23,7 +25,20 @@ namespace Ombi.Api.Discord
|
|||
|
||||
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; }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ 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))
|
||||
{
|
||||
|
|
146
src/Ombi.Core.Tests/Services/PlexServiceTests.cs
Normal file
146
src/Ombi.Core.Tests/Services/PlexServiceTests.cs
Normal file
|
@ -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().Object);
|
||||
|
||||
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().Object);
|
||||
|
||||
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().Object);
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
202
src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs
Normal file
202
src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs
Normal file
|
@ -0,0 +1,202 @@
|
|||
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.Text;
|
||||
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().Object);
|
||||
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
|
||||
|
||||
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().Object);
|
||||
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
|
||||
|
||||
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().Object);
|
||||
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
|
||||
|
||||
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);
|
||||
var albums = _fixture.CreateMany<AlbumRequest>(10);
|
||||
var chilRequests = _fixture.CreateMany<ChildRequests>(10);
|
||||
|
||||
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", 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);
|
||||
|
||||
CollectionAssert.IsEmpty(result.Where(x => !string.IsNullOrEmpty(x.Username) && !string.IsNullOrEmpty(x.UserId)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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).FirstOrDefaultAsync();
|
||||
var userTv = children.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefaultAsync();
|
||||
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 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());
|
||||
}
|
||||
}
|
||||
}
|
16
src/Ombi.Core/Models/PlexUserWatchlistModel.cs
Normal file
16
src/Ombi.Core/Models/PlexUserWatchlistModel.cs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
24
src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs
Normal file
24
src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -157,7 +157,6 @@ namespace Ombi.Core.Senders
|
|||
}
|
||||
|
||||
int qualityToUse;
|
||||
var sonarrV3 = s.V3;
|
||||
var languageProfileId = s.LanguageProfile;
|
||||
string rootFolderPath;
|
||||
string seriesType;
|
||||
|
@ -265,13 +264,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
|
||||
};
|
||||
|
||||
if (sonarrV3)
|
||||
{
|
||||
newSeries.languageProfileId = languageProfileId;
|
||||
}
|
||||
|
||||
|
||||
// Montitor the correct seasons,
|
||||
// If we have that season in the model then it's monitored!
|
||||
|
|
12
src/Ombi.Core/Services/IPlexService.cs
Normal file
12
src/Ombi.Core/Services/IPlexService.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
12
src/Ombi.Core/Services/IRecentlyRequestedService.cs
Normal file
12
src/Ombi.Core/Services/IRecentlyRequestedService.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
55
src/Ombi.Core/Services/PlexService.cs
Normal file
55
src/Ombi.Core/Services/PlexService.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
183
src/Ombi.Core/Services/RecentlyRequestedService.cs
Normal file
183
src/Ombi.Core/Services/RecentlyRequestedService.cs
Normal file
|
@ -0,0 +1,183 @@
|
|||
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))
|
||||
{
|
||||
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 = hideUsers.Hide ? string.Empty : item.RequestedUserId,
|
||||
Username = hideUsers.Hide ? string.Empty : 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))
|
||||
{
|
||||
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 = hideUsers.Hide ? string.Empty : item.RequestedUserId,
|
||||
Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias,
|
||||
MediaId = item.ForeignAlbumId,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var item in await recentTvRequests.ToListAsync(cancellationToken))
|
||||
{
|
||||
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 = hideUsers.Hide ? string.Empty : item.RequestedUserId,
|
||||
Username = hideUsers.Hide ? string.Empty : 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -228,6 +228,8 @@ 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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -23,9 +23,13 @@ namespace Ombi.Hubs
|
|||
public static List<string> AdminConnectionIds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UsersOnline.Any())
|
||||
{
|
||||
return UsersOnline.Where(x => x.Value.Roles.Contains(OmbiRoles.Admin)).Select(x => x.Key).ToList();
|
||||
}
|
||||
return Enumerable.Empty<string>().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public const string NotificationEvent = "Notification";
|
||||
|
|
|
@ -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>
|
|
@ -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]
|
||||
|
|
|
@ -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 },
|
||||
};
|
||||
}
|
||||
}
|
192
src/Ombi.Schedule.Tests/AvailabilityCheckerTests.cs
Normal file
192
src/Ombi.Schedule.Tests/AvailabilityCheckerTests.cs
Normal file
|
@ -0,0 +1,192 @@
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
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().Object;
|
||||
|
||||
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().Object;
|
||||
|
||||
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, IHubContext<NotificationHub> hub) : base(tvRequest, notification, log, hub)
|
||||
{
|
||||
}
|
||||
|
||||
public new Task ProcessTvShow(IQueryable<IBaseMediaServerEpisode> seriesEpisodes, ChildRequests child) => base.ProcessTvShow(seriesEpisodes, child);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@
|
|||
<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>
|
||||
|
|
|
@ -19,49 +19,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>();
|
||||
}
|
||||
|
||||
|
||||
private Mock<IPlexContentRepository> _repo;
|
||||
private Mock<ITvRequestRepository> _tv;
|
||||
private Mock<IMovieRequestRepository> _movie;
|
||||
private Mock<INotificationHelper> _notify;
|
||||
private PlexAvailabilityChecker Checker;
|
||||
|
||||
[Test]
|
||||
public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex()
|
||||
public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex_WithImdbId()
|
||||
{
|
||||
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>(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
|
||||
_mocker.Setup<IPlexContentRepository, Task<PlexServerContent>>(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent());
|
||||
|
||||
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_ShouldMarkAvailable_WhenInPlex_WithTheMovieDbId()
|
||||
{
|
||||
var request = new MovieRequests
|
||||
{
|
||||
ImdbId = null,
|
||||
TheMovieDbId = 33
|
||||
};
|
||||
_mocker.Setup<IMovieRequestRepository>(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);
|
||||
});
|
||||
|
||||
_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_WithTheMovieDbId_4K_Enabled ()
|
||||
{
|
||||
_mocker.Setup<IFeatureService, Task<bool>>(x => x.FeatureEnabled(FeatureNames.Movie4KRequests)).ReturnsAsync(true);
|
||||
var request = new MovieRequests
|
||||
{
|
||||
ImdbId = "test"
|
||||
};
|
||||
_mocker.Setup<IMovieRequestRepository>(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 _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_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>(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 +162,96 @@ namespace Ombi.Schedule.Tests
|
|||
{
|
||||
ImdbId = "test"
|
||||
};
|
||||
_movie.Setup(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
|
||||
_mocker.Setup<IMovieRequestRepository>(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 = new ChildRequests
|
||||
var request = CreateChildRequest(null, 33, 99);
|
||||
_mocker.Setup<ITvRequestRepository>(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<IPlexContentRepository, IQueryable<IMediaServerEpisode>>(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
|
||||
{
|
||||
ParentRequest = new TvRequests { TvDbId = 1 },
|
||||
new PlexEpisode
|
||||
{
|
||||
Series = new PlexServerContent
|
||||
{
|
||||
TheMovieDbId = 33.ToString(),
|
||||
Title = "Test"
|
||||
},
|
||||
EpisodeNumber = 1,
|
||||
SeasonNumber = 2,
|
||||
}
|
||||
}.AsQueryable().BuildMock().Object);
|
||||
|
||||
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>(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock().Object);
|
||||
_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().Object);
|
||||
|
||||
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 = CreateChildRequest("abc", -1, 99);
|
||||
_mocker.Setup<ITvRequestRepository>(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock().Object);
|
||||
_mocker.Setup<IPlexContentRepository, IQueryable<IMediaServerEpisode>>(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
|
||||
{
|
||||
new PlexEpisode
|
||||
{
|
||||
Series = new PlexServerContent
|
||||
{
|
||||
Title = "UNITTEST",
|
||||
ImdbId = "invlaid",
|
||||
},
|
||||
EpisodeNumber = 1,
|
||||
SeasonNumber = 2,
|
||||
}
|
||||
}.AsQueryable().BuildMock().Object);
|
||||
|
||||
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
|
||||
|
@ -106,27 +274,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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
328
src/Ombi.Schedule.Tests/PlexUserImporterTests.cs
Normal file
328
src/Ombi.Schedule.Tests/PlexUserImporterTests.cs
Normal file
|
@ -0,0 +1,328 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using Moq;
|
||||
using MockQueryable.Moq;
|
||||
using Moq;
|
||||
using Moq.AutoMock;
|
||||
using NUnit.Framework;
|
||||
using Ombi.Api.Plex;
|
||||
|
@ -8,6 +9,7 @@ using Ombi.Core.Engine.Interfaces;
|
|||
using Ombi.Core.Models.Requests;
|
||||
using Ombi.Core.Settings;
|
||||
using Ombi.Core.Settings.Models.External;
|
||||
using Ombi.Core.Tests;
|
||||
using Ombi.Schedule.Jobs.Plex;
|
||||
using Ombi.Store.Entities;
|
||||
using Ombi.Store.Repository;
|
||||
|
@ -32,11 +34,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().Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -53,6 +56,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 +79,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().Object);
|
||||
|
||||
_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().Object);
|
||||
|
||||
_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 +171,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 +195,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 +246,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 +296,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
|
||||
{
|
||||
|
@ -273,6 +346,7 @@ namespace Ombi.Schedule.Tests
|
|||
[Test]
|
||||
public async Task TvRequestFromWatchList_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
|
||||
{
|
||||
|
@ -371,6 +446,7 @@ namespace Ombi.Schedule.Tests
|
|||
[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 +496,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
|
||||
{
|
||||
|
|
|
@ -11,7 +11,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,17 +19,13 @@ 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(
|
||||
|
@ -42,15 +37,12 @@ namespace Ombi.Schedule.Jobs.Radarr
|
|||
ILogger<ArrAvailabilityChecker> log,
|
||||
ISettingsService<RadarrSettings> radarrSettings,
|
||||
ISettingsService<SonarrSettings> sonarrSettings)
|
||||
: base(tvRequest, notification, log, hub)
|
||||
{
|
||||
_radarrRepo = radarrRepo;
|
||||
_sonarrRepo = sonarrRepo;
|
||||
_sonarrEpisodeRepo = sonarrEpisodeRepo;
|
||||
_notification = notification;
|
||||
_hub = hub;
|
||||
_tvRequest = tvRequest;
|
||||
_movies = movies;
|
||||
_logger = log;
|
||||
_radarrSettings = radarrSettings;
|
||||
_sonarrSettings = sonarrSettings;
|
||||
}
|
||||
|
@ -82,7 +74,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
|
||||
|
@ -114,7 +106,7 @@ namespace Ombi.Schedule.Jobs.Radarr
|
|||
}
|
||||
foreach (var item in itemsForAvailability)
|
||||
{
|
||||
await _notification.Notify(new NotificationOptions
|
||||
await _notificationService.Notify(new NotificationOptions
|
||||
{
|
||||
DateTime = DateTime.Now,
|
||||
NotificationType = NotificationType.RequestAvailable,
|
||||
|
@ -127,7 +119,7 @@ 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)
|
||||
|
@ -140,83 +132,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;
|
||||
}
|
||||
}
|
||||
await ProcessTvShow(seriesEpisodes, child);
|
||||
}
|
||||
|
||||
if (availableEpisode.Any())
|
||||
{
|
||||
await _tvRequest.Save();
|
||||
}
|
||||
|
||||
// Check to see if all of the episodes in all seasons are available for this request
|
||||
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
|
||||
if (allAvailable)
|
||||
{
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Sonarr Availability Checker found some new available Shows!");
|
||||
child.Available = true;
|
||||
child.MarkedAsAvailable = DateTime.UtcNow;
|
||||
_logger.LogInformation("[ARR_AC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}");
|
||||
// We have ful-fulled this request!
|
||||
await _tvRequest.Save();
|
||||
await _notification.Notify(new NotificationOptions
|
||||
{
|
||||
DateTime = DateTime.Now,
|
||||
NotificationType = NotificationType.RequestAvailable,
|
||||
RequestId = child.Id,
|
||||
RequestType = RequestType.TvShow,
|
||||
Recipient = child.RequestedUser.Email
|
||||
});
|
||||
}
|
||||
else if (availableEpisode.Any())
|
||||
{
|
||||
var notification = new NotificationOptions
|
||||
{
|
||||
DateTime = DateTime.Now,
|
||||
NotificationType = NotificationType.PartiallyAvailable,
|
||||
RequestId = child.Id,
|
||||
RequestType = RequestType.TvShow,
|
||||
Recipient = child.RequestedUser.Email,
|
||||
};
|
||||
notification.Substitutes.Add("Season", availableEpisode.First().SeasonNumber.ToString());
|
||||
notification.Substitutes.Add("Episodes", string.Join(", ", availableEpisode.Select(x => x.EpisodeNumber)));
|
||||
await _notification.Notify(notification);
|
||||
}
|
||||
}
|
||||
|
||||
await _tvRequest.Save();
|
||||
await _tvRepo.Save();
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
|
|
106
src/Ombi.Schedule/Jobs/AvailabilityChecker.cs
Normal file
106
src/Ombi.Schedule/Jobs/AvailabilityChecker.cs
Normal file
|
@ -0,0 +1,106 @@
|
|||
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.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 IHubContext<NotificationHub> _hub;
|
||||
|
||||
public AvailabilityChecker(ITvRequestRepository tvRequest, INotificationHelper notification,
|
||||
ILogger log, IHubContext<NotificationHub> hub)
|
||||
{
|
||||
_tvRepo = tvRequest;
|
||||
_notificationService = notification;
|
||||
_log = log;
|
||||
_hub = hub;
|
||||
}
|
||||
|
||||
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 _hub?.Clients?.Clients(NotificationHub.AdminConnectionIds)?
|
||||
.SendAsync(NotificationHub.NotificationEvent, "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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
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,38 +17,31 @@ 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)
|
||||
: 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)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Emby Availability Checker Started");
|
||||
await ProcessMovies();
|
||||
await ProcessTv();
|
||||
|
||||
_log.LogInformation("Finished Emby Availability Check");
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Emby Availability Checker Finished");
|
||||
}
|
||||
|
||||
|
@ -167,68 +157,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();
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
@ -45,38 +44,31 @@ 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)
|
||||
: 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)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Started");
|
||||
await ProcessMovies();
|
||||
await ProcessTv();
|
||||
|
||||
_log.LogInformation("Finished Jellyfin Availability Check");
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Finished");
|
||||
}
|
||||
|
||||
|
@ -193,68 +185,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();
|
||||
|
|
|
@ -10,7 +10,6 @@ using Ombi.Core.Services;
|
|||
using Ombi.Helpers;
|
||||
using Ombi.Hubs;
|
||||
using Ombi.Notifications.Models;
|
||||
using Ombi.Schedule.Jobs.Plex.Models;
|
||||
using Ombi.Settings.Settings.Models;
|
||||
using Ombi.Store.Entities;
|
||||
using Ombi.Store.Entities.Requests;
|
||||
|
@ -20,47 +19,39 @@ using Quartz;
|
|||
|
||||
namespace Ombi.Schedule.Jobs.Plex
|
||||
{
|
||||
public class PlexAvailabilityChecker : IPlexAvailabilityChecker
|
||||
public class PlexAvailabilityChecker : AvailabilityChecker, IPlexAvailabilityChecker
|
||||
{
|
||||
public PlexAvailabilityChecker(IPlexContentRepository repo, ITvRequestRepository tvRequest, IMovieRequestRepository movies,
|
||||
INotificationHelper notification, ILogger<PlexAvailabilityChecker> log, IHubContext<NotificationHub> hub, IFeatureService featureService)
|
||||
: base(tvRequest, notification, log, hub)
|
||||
{
|
||||
_tvRepo = tvRequest;
|
||||
_repo = repo;
|
||||
_movieRepo = movies;
|
||||
_notificationService = notification;
|
||||
_log = log;
|
||||
_notification = hub;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
private readonly ITvRequestRepository _tvRepo;
|
||||
private readonly IMovieRequestRepository _movieRepo;
|
||||
private readonly IPlexContentRepository _repo;
|
||||
private readonly INotificationHelper _notificationService;
|
||||
private readonly ILogger _log;
|
||||
private readonly IHubContext<NotificationHub> _notification;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public async Task Execute(IJobExecutionContext job)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Plex Availability Check Started");
|
||||
await ProcessMovies();
|
||||
await ProcessTv();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Plex Availability Check Failed");
|
||||
_log.LogError(e, "Exception thrown in Plex availbility checker");
|
||||
return;
|
||||
}
|
||||
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
await _hub.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Plex Availability Check Finished");
|
||||
}
|
||||
|
||||
|
@ -78,16 +69,22 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
{
|
||||
var useImdb = false;
|
||||
var useTvDb = false;
|
||||
var useMovieDb = false;
|
||||
if (child.ParentRequest.ImdbId.HasValue())
|
||||
{
|
||||
useImdb = true;
|
||||
}
|
||||
|
||||
if (child.ParentRequest.TvDbId.ToString().HasValue())
|
||||
if (child.ParentRequest.TvDbId > 0)
|
||||
{
|
||||
useTvDb = true;
|
||||
}
|
||||
|
||||
if (child.ParentRequest.ExternalProviderId > 0)
|
||||
{
|
||||
useMovieDb = true;
|
||||
}
|
||||
|
||||
var tvDbId = child.ParentRequest.TvDbId;
|
||||
var imdbId = child.ParentRequest.ImdbId;
|
||||
IQueryable<IMediaServerEpisode> seriesEpisodes = null;
|
||||
|
@ -99,83 +96,19 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
{
|
||||
seriesEpisodes = plexEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString());
|
||||
}
|
||||
|
||||
if (seriesEpisodes == null)
|
||||
if (useMovieDb && (seriesEpisodes == null || !seriesEpisodes.Any()))
|
||||
{
|
||||
continue;
|
||||
seriesEpisodes = plexEpisodes.Where(x => x.Series.TheMovieDbId == child.ParentRequest.ExternalProviderId.ToString());
|
||||
}
|
||||
|
||||
if (!seriesEpisodes.Any())
|
||||
if (seriesEpisodes == null || !seriesEpisodes.Any())
|
||||
{
|
||||
// Let's try and match the series by name
|
||||
seriesEpisodes = plexEpisodes.Where(x =>
|
||||
x.Series.Title == child.Title);
|
||||
|
||||
x.Series.Title.Equals(child.Title, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
child.Available = true;
|
||||
child.MarkedAsAvailable = DateTime.UtcNow;
|
||||
_log.LogInformation("[PAC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}");
|
||||
// We have ful-fulled this request!
|
||||
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)));
|
||||
await _notificationService.Notify(notification);
|
||||
}
|
||||
await ProcessTvShow(seriesEpisodes, child);
|
||||
}
|
||||
|
||||
await _tvRepo.Save();
|
||||
|
|
|
@ -124,7 +124,6 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
{
|
||||
await NotifyClient("Plex Sync - Checking if any requests are now available");
|
||||
Logger.LogInformation("Kicking off Plex Availability Checker");
|
||||
await _mediaCacheService.Purge();
|
||||
await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex");
|
||||
}
|
||||
var processedCont = processedContent?.Content?.Count() ?? 0;
|
||||
|
@ -133,6 +132,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
|
||||
await NotifyClient(recentlyAddedSearch ? $"Plex Recently Added Sync Finished, We processed {processedCont}, and {processedEp} Episodes" : "Plex Content Sync Finished");
|
||||
|
||||
await _mediaCacheService.Purge();
|
||||
}
|
||||
|
||||
private async Task<ProcessedContent> StartTheCache(PlexSettings plexSettings, bool recentlyAddedSearch)
|
||||
|
@ -496,31 +496,31 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
await Repo.Update(existingContent);
|
||||
}
|
||||
|
||||
// Just check the key
|
||||
if (existingKey != null)
|
||||
{
|
||||
// The rating key is all good!
|
||||
}
|
||||
else
|
||||
{
|
||||
// This means the rating key has changed somehow.
|
||||
// Should probably delete this and get the new one
|
||||
var oldKey = existingContent.Key;
|
||||
Repo.DeleteWithoutSave(existingContent);
|
||||
//// Just check the key
|
||||
//if (existingKey != null)
|
||||
//{
|
||||
// // The rating key is all good!
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// // This means the rating key has changed somehow.
|
||||
// // Should probably delete this and get the new one
|
||||
// var oldKey = existingContent.Key;
|
||||
// Repo.DeleteWithoutSave(existingContent);
|
||||
|
||||
// Because we have changed the rating key, we need to change all children too
|
||||
var episodeToChange = Repo.GetAllEpisodes().Cast<PlexEpisode>().Where(x => x.GrandparentKey == oldKey);
|
||||
if (episodeToChange.Any())
|
||||
{
|
||||
foreach (var e in episodeToChange)
|
||||
{
|
||||
Repo.DeleteWithoutSave(e);
|
||||
}
|
||||
}
|
||||
// // Because we have changed the rating key, we need to change all children too
|
||||
// var episodeToChange = Repo.GetAllEpisodes().Cast<PlexEpisode>().Where(x => x.GrandparentKey == oldKey);
|
||||
// if (episodeToChange.Any())
|
||||
// {
|
||||
// foreach (var e in episodeToChange)
|
||||
// {
|
||||
// Repo.DeleteWithoutSave(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
await Repo.SaveChangesAsync();
|
||||
existingContent = null;
|
||||
}
|
||||
// await Repo.SaveChangesAsync();
|
||||
// existingContent = null;
|
||||
//}
|
||||
}
|
||||
|
||||
// Also make sure it's not already being processed...
|
||||
|
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.SignalR;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Ombi.Api.Plex;
|
||||
using Ombi.Core.Authentication;
|
||||
using Ombi.Core.Settings;
|
||||
using Ombi.Core.Settings.Models.External;
|
||||
using Ombi.Helpers;
|
||||
|
@ -19,7 +20,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
{
|
||||
public class PlexUserImporter : IPlexUserImporter
|
||||
{
|
||||
public PlexUserImporter(IPlexApi api, UserManager<OmbiUser> um, ILogger<PlexUserImporter> log,
|
||||
public PlexUserImporter(IPlexApi api, OmbiUserManager um, ILogger<PlexUserImporter> log,
|
||||
ISettingsService<PlexSettings> plexSettings, ISettingsService<UserManagementSettings> ums, IHubContext<NotificationHub> hub)
|
||||
{
|
||||
_api = api;
|
||||
|
@ -33,7 +34,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
}
|
||||
|
||||
private readonly IPlexApi _api;
|
||||
private readonly UserManager<OmbiUser> _userManager;
|
||||
private readonly OmbiUserManager _userManager;
|
||||
private readonly ILogger<PlexUserImporter> _log;
|
||||
private readonly ISettingsService<PlexSettings> _plexSettings;
|
||||
private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
|
||||
|
@ -43,17 +44,17 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
public async Task Execute(IJobExecutionContext job)
|
||||
{
|
||||
var userManagementSettings = await _userManagementSettings.GetSettingsAsync();
|
||||
if (!userManagementSettings.ImportPlexUsers)
|
||||
if (!userManagementSettings.ImportPlexUsers && !userManagementSettings.ImportPlexAdmin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = await _plexSettings.GetSettingsAsync();
|
||||
if (!settings.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Plex User Importer Started");
|
||||
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.PlexUser).ToListAsync();
|
||||
|
@ -64,8 +65,22 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
continue;
|
||||
}
|
||||
|
||||
if (userManagementSettings.ImportPlexAdmin)
|
||||
{
|
||||
await ImportAdmin(userManagementSettings, server, allUsers);
|
||||
}
|
||||
if (userManagementSettings.ImportPlexUsers)
|
||||
{
|
||||
await ImportPlexUsers(userManagementSettings, allUsers, server);
|
||||
}
|
||||
}
|
||||
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Plex User Importer Finished");
|
||||
}
|
||||
|
||||
private async Task ImportPlexUsers(UserManagementSettings userManagementSettings, List<OmbiUser> allUsers, PlexServers server)
|
||||
{
|
||||
var users = await _api.GetUsers(server.PlexAuthToken);
|
||||
|
||||
foreach (var plexUser in users.User)
|
||||
|
@ -139,17 +154,8 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
}
|
||||
}
|
||||
|
||||
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
|
||||
.SendAsync(NotificationHub.NotificationEvent, "Plex User Importer Finished");
|
||||
}
|
||||
|
||||
private async Task ImportAdmin(UserManagementSettings settings, PlexServers server, List<OmbiUser> allUsers)
|
||||
{
|
||||
if (!settings.ImportPlexAdmin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plexAdmin = (await _api.GetAccount(server.PlexAuthToken)).user;
|
||||
|
||||
// Check if the admin is already in the DB
|
||||
|
@ -166,6 +172,14 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
return;
|
||||
}
|
||||
|
||||
// Ensure we don't have a user with the same username
|
||||
var normalUsername = plexAdmin.username.ToUpperInvariant();
|
||||
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == normalUsername))
|
||||
{
|
||||
_log.LogWarning($"Cannot add user {plexAdmin.username} because their username is already in Ombi, skipping this user");
|
||||
return;
|
||||
}
|
||||
|
||||
var newUser = new OmbiUser
|
||||
{
|
||||
UserType = UserType.PlexUser,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Ombi.Api.Plex;
|
||||
using Ombi.Api.Plex.Models;
|
||||
|
@ -32,10 +33,11 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
private readonly IHubContext<NotificationHub> _hub;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo;
|
||||
private readonly IRepository<PlexWatchlistUserError> _userError;
|
||||
|
||||
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
|
||||
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, IHubContext<NotificationHub> hub,
|
||||
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo)
|
||||
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError)
|
||||
{
|
||||
_plexApi = plexApi;
|
||||
_settings = settings;
|
||||
|
@ -45,6 +47,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
_hub = hub;
|
||||
_logger = logger;
|
||||
_watchlistRepo = watchlistRepo;
|
||||
_userError = userError;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
|
@ -64,9 +67,35 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
{
|
||||
try
|
||||
{
|
||||
// Check if the user has errors and the token is the same (not refreshed)
|
||||
var failedUser = await _userError.GetAll().Where(x => x.UserId == user.Id).FirstOrDefaultAsync();
|
||||
if (failedUser != null)
|
||||
{
|
||||
if (failedUser.MediaServerToken.Equals(user.MediaServerToken))
|
||||
{
|
||||
_logger.LogInformation($"Skipping Plex Watchlist Import for user '{user.UserName}' as they failed previously and the token has not yet been refreshed");
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// remove that guy
|
||||
await _userError.Delete(failedUser);
|
||||
failedUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
|
||||
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
||||
if (watchlist?.AuthError ?? false)
|
||||
{
|
||||
_logger.LogError($"Auth failed for user '{user.UserName}'. Need to re-authenticate with Ombi.");
|
||||
await _userError.Add(new PlexWatchlistUserError
|
||||
{
|
||||
UserId = user.Id,
|
||||
MediaServerToken = user.MediaServerToken,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (watchlist == null || !(watchlist.MediaContainer?.Metadata?.Any() ?? false))
|
||||
{
|
||||
_logger.LogDebug($"No watchlist found for {user.UserName}");
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace Ombi.Schedule.Jobs.Sonarr
|
|||
{
|
||||
public class SonarrSync : ISonarrSync
|
||||
{
|
||||
public SonarrSync(ISettingsService<SonarrSettings> s, ISonarrApi api, ILogger<SonarrSync> l, ExternalContext ctx,
|
||||
public SonarrSync(ISettingsService<SonarrSettings> s, ISonarrV3Api api, ILogger<SonarrSync> l, ExternalContext ctx,
|
||||
IMovieDbApi movieDbApi)
|
||||
{
|
||||
_settings = s;
|
||||
|
@ -35,7 +35,7 @@ namespace Ombi.Schedule.Jobs.Sonarr
|
|||
}
|
||||
|
||||
private readonly ISettingsService<SonarrSettings> _settings;
|
||||
private readonly ISonarrApi _api;
|
||||
private readonly ISonarrV3Api _api;
|
||||
private readonly ILogger<SonarrSync> _log;
|
||||
private readonly ExternalContext _ctx;
|
||||
private readonly IMovieDbApi _movieDbApi;
|
||||
|
@ -74,8 +74,6 @@ namespace Ombi.Schedule.Jobs.Sonarr
|
|||
}
|
||||
});
|
||||
|
||||
var existingSeries = await _ctx.SonarrCache.Select(x => x.TvDbId).ToListAsync();
|
||||
|
||||
var sonarrCacheToSave = new HashSet<SonarrCache>();
|
||||
foreach (var id in ids)
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
public bool UseCustomPage { get; set; }
|
||||
public bool HideAvailableFromDiscover { get; set; }
|
||||
public string Favicon { get; set; }
|
||||
public bool HideAvailableRecentlyRequested { get; set; }
|
||||
|
||||
public string AddToUrl(string part)
|
||||
{
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
public string QualityProfileAnime { get; set; }
|
||||
public string RootPathAnime { get; set; }
|
||||
public bool AddOnly { get; set; }
|
||||
public bool V3 { get; set; }
|
||||
public int LanguageProfile { get; set; }
|
||||
public int LanguageProfileAnime { get; set; }
|
||||
public bool ScanForAvailability { get; set; }
|
||||
|
|
|
@ -45,6 +45,7 @@ namespace Ombi.Store.Context
|
|||
public DbSet<RequestLog> RequestLogs { get; set; }
|
||||
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
|
||||
public DbSet<Votes> Votes { get; set; }
|
||||
public DbSet<PlexWatchlistUserError> PlexWatchListUserError { get; set; }
|
||||
|
||||
|
||||
public DbSet<Audit> Audit { get; set; }
|
||||
|
@ -212,7 +213,7 @@ namespace Ombi.Store.Context
|
|||
notificationToAdd = new NotificationTemplates
|
||||
{
|
||||
NotificationType = notificationType,
|
||||
Message = "Your TV request for {Title} is now partially available! Season {PartiallyAvailableSeasonNumber} Episodes {PartiallyAvailableEpisodeNumbers}!",
|
||||
Message = "Your TV request for {Title} is now partially available! Episodes {PartiallyAvailableEpisodesList}!",
|
||||
Subject = "{ApplicationName}: Partially Available Request!",
|
||||
Agent = agent,
|
||||
Enabled = true,
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
|||
|
||||
namespace Ombi.Store.Entities
|
||||
{
|
||||
public interface IMediaServerContent: IEntity
|
||||
public interface IMediaServerContent : IEntity
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
|
@ -29,10 +29,8 @@ namespace Ombi.Store.Entities
|
|||
public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId);
|
||||
}
|
||||
|
||||
public interface IMediaServerEpisode
|
||||
public interface IMediaServerEpisode : IBaseMediaServerEpisode
|
||||
{
|
||||
public int EpisodeNumber { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
public string Title { get; set; }
|
||||
/// <summary>
|
||||
/// The Season key
|
||||
|
@ -47,6 +45,12 @@ namespace Ombi.Store.Entities
|
|||
public bool IsIn(IMediaServerContent content);
|
||||
}
|
||||
|
||||
public interface IBaseMediaServerEpisode
|
||||
{
|
||||
public int EpisodeNumber { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
}
|
||||
|
||||
public enum MediaType
|
||||
{
|
||||
Movie = 0,
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace Ombi.Store.Entities
|
|||
public abstract RecentlyAddedType RecentlyAddedType { get; }
|
||||
}
|
||||
|
||||
public abstract class MediaServerEpisode: Entity, IMediaServerEpisode
|
||||
public abstract class MediaServerEpisode : Entity, IMediaServerEpisode
|
||||
{
|
||||
public int EpisodeNumber { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
|
|
11
src/Ombi.Store/Entities/PlexWatchlistUserError.cs
Normal file
11
src/Ombi.Store/Entities/PlexWatchlistUserError.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Ombi.Store.Entities
|
||||
{
|
||||
[Table(nameof(PlexWatchlistUserError))]
|
||||
public class PlexWatchlistUserError : Entity
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public string MediaServerToken { get; set; }
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace Ombi.Store.Entities
|
||||
{
|
||||
[Table("SonarrEpisodeCache")]
|
||||
public class SonarrEpisodeCache : Entity
|
||||
public class SonarrEpisodeCache : Entity, IBaseMediaServerEpisode
|
||||
{
|
||||
public int SeasonNumber { get; set; }
|
||||
public int EpisodeNumber { get; set; }
|
||||
|
|
1302
src/Ombi.Store/Migrations/OmbiMySql/20220823190345_PlexWatchlistUserError.Designer.cs
generated
Normal file
1302
src/Ombi.Store/Migrations/OmbiMySql/20220823190345_PlexWatchlistUserError.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Ombi.Store.Migrations.OmbiMySql
|
||||
{
|
||||
public partial class PlexWatchlistUserError : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PlexWatchlistUserError",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
UserId = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
MediaServerToken = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlexWatchlistUserError", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PlexWatchlistUserError");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -350,6 +350,23 @@ namespace Ombi.Store.Migrations.OmbiMySql
|
|||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistUserError", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("MediaServerToken")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PlexWatchlistUserError");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
1300
src/Ombi.Store/Migrations/OmbiSqlite/20220823192128_PlexWatchlistUserError.Designer.cs
generated
Normal file
1300
src/Ombi.Store/Migrations/OmbiSqlite/20220823192128_PlexWatchlistUserError.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,32 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Ombi.Store.Migrations.OmbiSqlite
|
||||
{
|
||||
public partial class PlexWatchlistUserError : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PlexWatchlistUserError",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
MediaServerToken = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlexWatchlistUserError", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PlexWatchlistUserError");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -348,6 +348,23 @@ namespace Ombi.Store.Migrations.OmbiSqlite
|
|||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistUserError", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MediaServerToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PlexWatchlistUserError");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
21
src/Ombi.Tests/SignalRHelper.cs
Normal file
21
src/Ombi.Tests/SignalRHelper.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
using Moq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ombi.Tests
|
||||
{
|
||||
public class SignalRHelper
|
||||
{
|
||||
public static Mock<IHubContext<T>> MockHub<T>() where T : Hub
|
||||
{
|
||||
Mock<IHubClients> mockClients = new Mock<IHubClients>();
|
||||
Mock<IClientProxy> mockClientProxy = new Mock<IClientProxy>();
|
||||
mockClients.Setup(clients => clients.Clients(It.IsAny<IReadOnlyList<string>>())).Returns(mockClientProxy.Object);
|
||||
|
||||
var hubContext = new Mock<IHubContext<T>>();
|
||||
hubContext.Setup(x => x.Clients).Returns(() => mockClients.Object);
|
||||
|
||||
return hubContext;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,5 +46,7 @@ namespace Ombi.Api.TheMovieDb
|
|||
Task<List<Language>> GetLanguages(CancellationToken cancellationToken);
|
||||
Task<List<WatchProvidersResults>> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken);
|
||||
Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, int page, CancellationToken cancellationToken);
|
||||
Task<MovieDbImages> GetTvImages(string theMovieDbId, CancellationToken token);
|
||||
Task<MovieDbImages> GetMovieImages(string theMovieDbId, CancellationToken token);
|
||||
}
|
||||
}
|
||||
|
|
44
src/Ombi.TheMovieDbApi/Models/TvImages.cs
Normal file
44
src/Ombi.TheMovieDbApi/Models/TvImages.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
namespace Ombi.Api.TheMovieDb.Models
|
||||
{
|
||||
public class MovieDbImages
|
||||
{
|
||||
public Backdrop[] backdrops { get; set; }
|
||||
public int id { get; set; }
|
||||
public Logo[] logos { get; set; }
|
||||
public Poster[] posters { get; set; }
|
||||
}
|
||||
|
||||
public class Backdrop
|
||||
{
|
||||
public float aspect_ratio { get; set; }
|
||||
public int height { get; set; }
|
||||
public string iso_639_1 { get; set; }
|
||||
public string file_path { get; set; }
|
||||
public float vote_average { get; set; }
|
||||
public int vote_count { get; set; }
|
||||
public int width { get; set; }
|
||||
}
|
||||
|
||||
public class Logo
|
||||
{
|
||||
public float aspect_ratio { get; set; }
|
||||
public int height { get; set; }
|
||||
public string iso_639_1 { get; set; }
|
||||
public string file_path { get; set; }
|
||||
public float vote_average { get; set; }
|
||||
public int vote_count { get; set; }
|
||||
public int width { get; set; }
|
||||
}
|
||||
|
||||
public class Poster
|
||||
{
|
||||
public float aspect_ratio { get; set; }
|
||||
public int height { get; set; }
|
||||
public string iso_639_1 { get; set; }
|
||||
public string file_path { get; set; }
|
||||
public float vote_average { get; set; }
|
||||
public int vote_count { get; set; }
|
||||
public int width { get; set; }
|
||||
}
|
||||
|
||||
}
|
|
@ -514,6 +514,22 @@ namespace Ombi.Api.TheMovieDb
|
|||
return Api.Request<WatchProviders>(request, token);
|
||||
}
|
||||
|
||||
public Task<MovieDbImages> GetTvImages(string theMovieDbId, CancellationToken token)
|
||||
{
|
||||
var request = new Request($"tv/{theMovieDbId}/images", BaseUri, HttpMethod.Get);
|
||||
request.AddQueryString("api_key", ApiToken);
|
||||
|
||||
return Api.Request<MovieDbImages>(request, token);
|
||||
}
|
||||
|
||||
public Task<MovieDbImages> GetMovieImages(string theMovieDbId, CancellationToken token)
|
||||
{
|
||||
var request = new Request($"movie/{theMovieDbId}/images", BaseUri, HttpMethod.Get);
|
||||
request.AddQueryString("api_key", ApiToken);
|
||||
|
||||
return Api.Request<MovieDbImages>(request, token);
|
||||
}
|
||||
|
||||
private async Task AddDiscoverSettings(Request request)
|
||||
{
|
||||
var settings = await Settings;
|
||||
|
|
3
src/Ombi/.vscode/settings.json
vendored
3
src/Ombi/.vscode/settings.json
vendored
|
@ -22,7 +22,8 @@
|
|||
"emby",
|
||||
"availability-rules",
|
||||
"details",
|
||||
"requests"
|
||||
"requests",
|
||||
"sonarr"
|
||||
],
|
||||
"rpc.enabled": true
|
||||
}
|
||||
|
|
|
@ -6,11 +6,15 @@ module.exports = {
|
|||
"addons": [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions"
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/preset-scss",
|
||||
],
|
||||
"framework": "@storybook/angular",
|
||||
"core": {
|
||||
"builder": "@storybook/builder-webpack5"
|
||||
},
|
||||
"staticDirs": ['../../wwwroot/images']
|
||||
"staticDirs": [{ from: '../../wwwroot/images', to: 'images' }, { from: '../../wwwroot/translations', to: 'translations'}],
|
||||
"features": {
|
||||
interactionsDebugger: true,
|
||||
}
|
||||
}
|
|
@ -2,4 +2,9 @@
|
|||
.test-class {
|
||||
background-color: purple;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #0f171f;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,8 @@
|
|||
import { setCompodocJson } from "@storybook/addon-docs/angular";
|
||||
import docJson from "../documentation.json";
|
||||
|
||||
import '../src/styles/_imports.scss';
|
||||
|
||||
setCompodocJson(docJson);
|
||||
|
||||
export const parameters = {
|
||||
|
|
|
@ -26,12 +26,12 @@
|
|||
"@angular/platform-server": "^14.0.0",
|
||||
"@angular/router": "^14.0.0",
|
||||
"@angularclass/hmr": "^3.0.0",
|
||||
"@microsoft/signalr": "^6.0.7",
|
||||
"@auth0/angular-jwt": "^5.0.2",
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"@fullcalendar/core": "^4.2.0",
|
||||
"@fullcalendar/daygrid": "^4.4.0",
|
||||
"@fullcalendar/interaction": "^4.2.0",
|
||||
"@microsoft/signalr": "^6.0.7",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@ngx-translate/http-loader": "^7.0.0",
|
||||
"@ngxs/devtools-plugin": "^3.7.3",
|
||||
|
@ -57,16 +57,16 @@
|
|||
"popper.js": "^1.14.3",
|
||||
"primeicons": "^5.0.0",
|
||||
"primeng": "^13.2.0",
|
||||
"protractor": "~5.4.0",
|
||||
"rxjs": "^7.5.4",
|
||||
"sass-recursive-map-merge": "^1.0.1",
|
||||
"store": "^2.0.12",
|
||||
"ts-md5": "^1.2.7",
|
||||
"tslib": "^1.10.0",
|
||||
"tslint-angular": "^1.1.2",
|
||||
"zone.js": "~0.11.4",
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~5.0.1",
|
||||
"tslint": "^5.12.0"
|
||||
"tslib": "^1.10.0",
|
||||
"tslint": "^5.12.0",
|
||||
"tslint-angular": "^1.1.2",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^14.0.0",
|
||||
|
@ -75,7 +75,6 @@
|
|||
"@angular/language-service": "^14.0.0",
|
||||
"@babel/core": "^7.18.9",
|
||||
"@compodoc/compodoc": "^1.1.19",
|
||||
"@types/node": "^16.11.45",
|
||||
"@storybook/addon-actions": "^6.5.9",
|
||||
"@storybook/addon-essentials": "^6.5.9",
|
||||
"@storybook/addon-interactions": "^6.5.9",
|
||||
|
@ -84,8 +83,10 @@
|
|||
"@storybook/builder-webpack5": "^6.5.9",
|
||||
"@storybook/manager-webpack5": "^6.5.9",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/preset-scss": "^1.0.3",
|
||||
"@types/jasmine": "~3.6.7",
|
||||
"@types/jasminewd2": "~2.0.8",
|
||||
"@types/node": "^16.11.45",
|
||||
"babel-loader": "^8.2.5",
|
||||
"chromatic": "^6.7.1",
|
||||
"codelyzer": "^6.0.1",
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { Story, Meta, moduleMetadata } from '@storybook/angular';
|
||||
import { ButtonComponent } from './button.component';
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'Button Component',
|
||||
component: ButtonComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: {}
|
||||
},
|
||||
MatButtonModule
|
||||
]
|
||||
})
|
||||
]
|
||||
} as Meta;
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
|
||||
const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
text: 'Primary',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
Secondary.args = {
|
||||
type: 'accent',
|
||||
text: 'Secondary',
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import { OmbiCommonModules } from "../modules";
|
||||
import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'ombi-button',
|
||||
imports: [...OmbiCommonModules, MatButtonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<button [id]="id" [type]="type" [class]="class" [data-toggle]="dataToggle" mat-raised-button [data-target]="dataTarget">{{text}}</button>
|
||||
`
|
||||
})
|
||||
export class ButtonComponent {
|
||||
|
||||
@Input() public text: string;
|
||||
|
||||
@Input() public id: string;
|
||||
@Input() public type: string = "primary";
|
||||
@Input() public class: string;
|
||||
@Input('data-toggle') public dataToggle: string;
|
||||
@Input('data-target') public dataTarget: string;
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<div id="detailed-{{request.mediaId}}" class="detailed-container" [style.background-image]="background">
|
||||
<div class="row">
|
||||
<div class="col-xl-5 col-lg-5 col-md-5 col-sm-12 posterColumn">
|
||||
<ombi-image (click)="click()" [src]="request.posterPath" [type]="request.type" class="poster" alt="{{request.title}}">
|
||||
</ombi-image>
|
||||
</div>
|
||||
<div class="col-xl-7 col-lg-7 col-md-7 col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-12 title">
|
||||
<div class="text-right year"><sup>{{request.releaseDate | date:'yyyy'}}</sup></div>
|
||||
<h3 id="detailed-request-title-{{request.mediaId}}">{{request.title}}</h3>
|
||||
</div>
|
||||
<div class="col-12" *ngIf="request.username">
|
||||
<p id="detailed-request-requestedby-{{request.mediaId}}">{{'MediaDetails.RequestedBy' | translate}} {{request.username}}</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p id="detailed-request-date-{{request.mediaId}}">{{'MediaDetails.OnDate' | translate}} {{request.requestDate | amFromUtc | amLocal | amUserLocale | amDateFormat: 'l LT'}}</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p id="detailed-request-status-{{request.mediaId}}">{{'MediaDetails.Status' | translate}} <span class="badge badge-{{getClass(request)}}">{{getStatus(request) | translate}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row action-items">
|
||||
<div class="col-12" *ngIf="isAdmin">
|
||||
<button *ngIf="!request.approved" id="detailed-request-approve-{{request.mediaId}}" color="accent" mat-raised-button (click)="approve()">{{'Common.Approve' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
@import "~styles/variables.scss";
|
||||
|
||||
.detailed-container {
|
||||
width: 400px;
|
||||
height: 250px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid $ombi-background-primary-accent;
|
||||
|
||||
@media (max-width:768px) {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
background-color: $ombi-background-accent;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
|
||||
.overview {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.year {
|
||||
@media (max-width:768px) {
|
||||
padding-top: 3%;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .poster {
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
height: 225px;
|
||||
width: 100%;
|
||||
transition: .5s ease;
|
||||
backface-visibility: hidden;
|
||||
border: 1px solid #35465c;
|
||||
}
|
||||
|
||||
.action-items {
|
||||
@media (min-width:768px) {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
|
||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||
import { Story, Meta, moduleMetadata } from '@storybook/angular';
|
||||
import { IRecentlyRequested, RequestType } from '../../interfaces';
|
||||
import { DetailedCardComponent } from './detailed-card.component';
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
import { ImageService } from "../../services/image.service";
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { PipeModule } from '../../pipes/pipe.module';
|
||||
import { ImageComponent } from '../image/image.component';
|
||||
|
||||
function imageServiceMock(): Partial<ImageService> {
|
||||
return {
|
||||
getMoviePoster: () : Observable<string> => of("https://assets.fanart.tv/fanart/movies/603/movieposter/the-matrix-52256ae1021be.jpg"),
|
||||
getMovieBackground : () : Observable<string> => of("https://assets.fanart.tv/fanart/movies/603/movieposter/the-matrix-52256ae1021be.jpg"),
|
||||
getTmdbTvPoster : () : Observable<string> => of("/bfxwMdQyJc0CL24m5VjtWAN30mt.jpg"),
|
||||
getTmdbTvBackground : () : Observable<string> => of("/bfxwMdQyJc0CL24m5VjtWAN30mt.jpg"),
|
||||
};
|
||||
}
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'Detailed Card Component',
|
||||
component: DetailedCardComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: ImageService,
|
||||
useValue: imageServiceMock()
|
||||
}
|
||||
],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
CommonModule,
|
||||
ImageComponent,
|
||||
SharedModule,
|
||||
PipeModule
|
||||
]
|
||||
})
|
||||
]
|
||||
} as Meta;
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
|
||||
const Template: Story<DetailedCardComponent> = (args: DetailedCardComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
|
||||
export const NewMovieRequest = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
NewMovieRequest.args = {
|
||||
request: {
|
||||
title: 'The Matrix',
|
||||
approved: false,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.movie,
|
||||
mediaId: '603',
|
||||
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
isAdmin: false,
|
||||
};
|
||||
|
||||
export const MovieNoUsername = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
MovieNoUsername.args = {
|
||||
request: {
|
||||
title: 'The Matrix',
|
||||
approved: false,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
userId: '12345',
|
||||
type: RequestType.movie,
|
||||
mediaId: '603',
|
||||
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
export const AvailableMovie = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
AvailableMovie.args = {
|
||||
request: {
|
||||
title: 'The Matrix',
|
||||
approved: false,
|
||||
available: true,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.movie,
|
||||
mediaId: '603',
|
||||
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
export const ApprovedMovie = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
ApprovedMovie.args = {
|
||||
request: {
|
||||
title: 'The Matrix',
|
||||
approved: true,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.movie,
|
||||
mediaId: '603',
|
||||
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
export const NewTvRequest = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
NewTvRequest.args = {
|
||||
request: {
|
||||
title: 'For All Mankind',
|
||||
approved: false,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.tvShow,
|
||||
mediaId: '603',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
|
||||
export const ApprovedTv = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
ApprovedTv.args = {
|
||||
request: {
|
||||
title: 'For All Mankind',
|
||||
approved: true,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.tvShow,
|
||||
mediaId: '603',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
export const AvailableTv = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
AvailableTv.args = {
|
||||
request: {
|
||||
title: 'For All Mankind',
|
||||
approved: true,
|
||||
available: true,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.tvShow,
|
||||
mediaId: '603',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
|
||||
export const PartiallyAvailableTv = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
PartiallyAvailableTv.args = {
|
||||
request: {
|
||||
title: 'For All Mankind',
|
||||
approved: true,
|
||||
available: false,
|
||||
tvPartiallyAvailable: true,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.tvShow,
|
||||
mediaId: '603',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
export const TvNoUsername = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
TvNoUsername.args = {
|
||||
request: {
|
||||
title: 'For All Mankind',
|
||||
approved: true,
|
||||
available: false,
|
||||
tvPartiallyAvailable: true,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
userId: '12345',
|
||||
type: RequestType.tvShow,
|
||||
mediaId: '603',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
};
|
||||
|
||||
export const AdminNewMovie = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
AdminNewMovie.args = {
|
||||
request: {
|
||||
title: 'The Matrix',
|
||||
approved: false,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.movie,
|
||||
mediaId: '603',
|
||||
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
isAdmin: true,
|
||||
};
|
||||
|
||||
export const AdminTvShow = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
AdminTvShow.args = {
|
||||
request: {
|
||||
title: 'For All Mankind',
|
||||
approved: false,
|
||||
available: false,
|
||||
tvPartiallyAvailable: true,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
userId: '12345',
|
||||
type: RequestType.tvShow,
|
||||
mediaId: '603',
|
||||
username: 'John Doe',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
isAdmin: true,
|
||||
};
|
||||
|
||||
export const AdminApprovedMovie = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||
AdminApprovedMovie.args = {
|
||||
request: {
|
||||
title: 'The Matrix',
|
||||
approved: true,
|
||||
available: false,
|
||||
tvPartiallyAvailable: false,
|
||||
requestDate: new Date(2022, 1, 1),
|
||||
username: 'John Doe',
|
||||
userId: '12345',
|
||||
type: RequestType.movie,
|
||||
mediaId: '603',
|
||||
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
|
||||
releaseDate: new Date(2020, 1, 1),
|
||||
} as IRecentlyRequested,
|
||||
isAdmin: true,
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { IRecentlyRequested, RequestType } from "../../interfaces";
|
||||
import { ImageService } from "app/services";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
|
||||
|
||||
@Component({
|
||||
standalone: false,
|
||||
selector: 'ombi-detailed-card',
|
||||
templateUrl: './detailed-card.component.html',
|
||||
styleUrls: ['./detailed-card.component.scss']
|
||||
})
|
||||
export class DetailedCardComponent implements OnInit, OnDestroy {
|
||||
@Input() public request: IRecentlyRequested;
|
||||
@Input() public isAdmin: boolean = false;
|
||||
@Output() public onClick: EventEmitter<void> = new EventEmitter<void>();
|
||||
@Output() public onApprove: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
public RequestType = RequestType;
|
||||
public loading: false;
|
||||
|
||||
private $imageSub = new Subject<void>();
|
||||
|
||||
public background: SafeStyle;
|
||||
|
||||
constructor(private imageService: ImageService, private sanitizer: DomSanitizer) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.request.posterPath) {
|
||||
this.loadImages();
|
||||
} else {
|
||||
this.request.posterPath = `https://image.tmdb.org/t/p/w300${this.request.posterPath}`;
|
||||
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(https://image.tmdb.org/t/p/w300" + this.request.background + ")");
|
||||
}
|
||||
}
|
||||
|
||||
public getStatus(request: IRecentlyRequested) {
|
||||
if (request.available) {
|
||||
return "Common.Available";
|
||||
}
|
||||
if (request.tvPartiallyAvailable) {
|
||||
return "Common.PartiallyAvailable";
|
||||
}
|
||||
if (request.approved) {
|
||||
return "Common.Approved";
|
||||
} else {
|
||||
return "Common.Pending";
|
||||
}
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.onClick.emit();
|
||||
}
|
||||
|
||||
public approve() {
|
||||
this.onApprove.emit();
|
||||
}
|
||||
|
||||
public getClass(request: IRecentlyRequested) {
|
||||
if (request.available || request.tvPartiallyAvailable) {
|
||||
return "success";
|
||||
}
|
||||
if (request.approved) {
|
||||
return "primary";
|
||||
} else {
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.$imageSub.next();
|
||||
this.$imageSub.complete();
|
||||
}
|
||||
|
||||
private loadImages() {
|
||||
switch (this.request.type) {
|
||||
case RequestType.movie:
|
||||
this.imageService.getMoviePoster(this.request.mediaId).pipe(takeUntil(this.$imageSub)).subscribe(x => this.request.posterPath = x);
|
||||
this.imageService.getMovieBackground(this.request.mediaId).pipe(takeUntil(this.$imageSub)).subscribe(x => {
|
||||
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(" + x + ")");
|
||||
});
|
||||
break;
|
||||
case RequestType.tvShow:
|
||||
this.imageService.getTmdbTvPoster(Number(this.request.mediaId)).pipe(takeUntil(this.$imageSub)).subscribe(x => this.request.posterPath = `https://image.tmdb.org/t/p/w300${x}`);
|
||||
this.imageService.getTmdbTvBackground(Number(this.request.mediaId)).pipe(takeUntil(this.$imageSub)).subscribe(x => {
|
||||
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(https://image.tmdb.org/t/p/w300" + x + ")");
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +1 @@
|
|||
<img src="{{src}}" (onError)="onError($event)" [class]="class" [id]="id" [style]="style"/>
|
||||
<img src="{{src}}" (error)="onError($event)" [class]="class" [id]="id" [style]="style"/>
|
||||
|
|
|
@ -28,6 +28,8 @@ import { APP_BASE_HREF } from "@angular/common";
|
|||
private defaultMovie = "/images/default_movie_poster.png";
|
||||
private defaultMusic = "i/mages/default-music-placeholder.png";
|
||||
|
||||
private alreadyErrored = false;
|
||||
|
||||
constructor (@Inject(APP_BASE_HREF) public href: string) {
|
||||
if (this.href.length > 1) {
|
||||
this.baseUrl = this.href;
|
||||
|
@ -35,6 +37,9 @@ import { APP_BASE_HREF } from "@angular/common";
|
|||
}
|
||||
|
||||
public onError(event: any) {
|
||||
if (this.alreadyErrored) {
|
||||
return;
|
||||
}
|
||||
// set to a placeholder
|
||||
switch(this.type) {
|
||||
case RequestType.movie:
|
||||
|
@ -48,10 +53,11 @@ import { APP_BASE_HREF } from "@angular/common";
|
|||
break;
|
||||
}
|
||||
|
||||
this.alreadyErrored = true;
|
||||
// Retry the original image
|
||||
const timeout = setTimeout(() => {
|
||||
event.target.src = this.src;
|
||||
clearTimeout(timeout);
|
||||
event.target.src = this.src;
|
||||
}, Math.floor(Math.random() * (7000 - 1000 + 1)) + 1000);
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./image-background/image-background.component";
|
||||
export * from "./image/image.component";
|
||||
export * from "./detailed-card/detailed-card.component";
|
|
@ -1,3 +1,4 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { MomentModule } from "ngx-moment";
|
||||
|
||||
export const OmbiCommonModules = [ CommonModule ];
|
||||
export const OmbiCommonModules = [ CommonModule, MomentModule ];
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { SearchV2Service } from "../../../services";
|
||||
import { IActorCredits, IActorCast } from "../../../interfaces/ISearchTvResultV2";
|
||||
import { IActorCredits, IActorCast, IActorCrew } from "../../../interfaces/ISearchTvResultV2";
|
||||
import { IDiscoverCardResult } from "../../interfaces";
|
||||
import { RequestType } from "../../../interfaces";
|
||||
import { AuthService } from "../../../auth/auth.service";
|
||||
|
@ -38,13 +38,13 @@ export class DiscoverActorComponent implements OnInit {
|
|||
this.searchService.getMoviesByActor(this.actorId),
|
||||
this.searchService.getTvByActor(this.actorId)
|
||||
]).subscribe(([movie, tv]) => {
|
||||
this.pushDiscoverResults(movie.cast, RequestType.movie);
|
||||
this.pushDiscoverResults(tv.cast, RequestType.tvShow);
|
||||
this.pushDiscoverResults(movie.crew, movie.cast, RequestType.movie);
|
||||
this.pushDiscoverResults(tv.crew, tv.cast, RequestType.tvShow);
|
||||
this.finishLoading();
|
||||
});
|
||||
}
|
||||
|
||||
pushDiscoverResults(cast: IActorCast[], type: RequestType) {
|
||||
pushDiscoverResults(crew: IActorCrew[], cast: IActorCast[], type: RequestType) {
|
||||
cast.forEach(m => {
|
||||
this.discoverResults.push({
|
||||
available: false,
|
||||
|
@ -62,6 +62,23 @@ export class DiscoverActorComponent implements OnInit {
|
|||
background: ""
|
||||
});
|
||||
});
|
||||
crew.forEach(m => {
|
||||
this.discoverResults.push({
|
||||
available: false,
|
||||
posterPath: m.poster_path ? `https://image.tmdb.org/t/p/w300/${m.poster_path}` : "../../../images/default_movie_poster.png",
|
||||
requested: false,
|
||||
title: m.title,
|
||||
type: type,
|
||||
id: m.id,
|
||||
url: null,
|
||||
rating: 0,
|
||||
overview: m.overview,
|
||||
approved: false,
|
||||
imdbid: "",
|
||||
denied: false,
|
||||
background: ""
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private loading() {
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
export const ResponsiveOptions = [
|
||||
{
|
||||
breakpoint: '1800px',
|
||||
numVisible: 5,
|
||||
numScroll: 4
|
||||
},
|
||||
{
|
||||
breakpoint: '1650px',
|
||||
numVisible: 3,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '1500px',
|
||||
numVisible: 3,
|
||||
numScroll: 3
|
||||
},
|
||||
{
|
||||
breakpoint: '900px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '768px',
|
||||
numVisible: 2,
|
||||
numScroll: 2
|
||||
},
|
||||
{
|
||||
breakpoint: '660px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '480px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
];
|
|
@ -1,5 +1,12 @@
|
|||
<div class="small-middle-container">
|
||||
|
||||
<div class="section">
|
||||
<h2>{{'Discovery.RecentlyRequestedTab' | translate}}</h2>
|
||||
<div>
|
||||
<ombi-recently-list [id]="'recentlyRequested'"></ombi-recently-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" [hidden]="!showSeasonal">
|
||||
<h2>{{'Discovery.SeasonalTab' | translate}}</h2>
|
||||
<div>
|
||||
|
@ -29,10 +36,5 @@
|
|||
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="section">
|
||||
<h2>{{'Discovery.RecentlyRequestedTab' | translate}}</h2>
|
||||
<div>
|
||||
<carousel-list [id]="'recentlyRequested'" [discoverType]="DiscoverType.RecentlyRequested"></carousel-list>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
|
@ -7,9 +7,11 @@ import { DiscoverCardComponent } from "./card/discover-card.component";
|
|||
import { DiscoverCollectionsComponent } from "./collections/discover-collections.component";
|
||||
import { DiscoverComponent } from "./discover/discover.component";
|
||||
import { DiscoverSearchResultsComponent } from "./search-results/search-results.component";
|
||||
import { RecentlyRequestedListComponent } from "./recently-requested-list/recently-requested-list.component";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { RequestServiceV2 } from "../../services/requestV2.service";
|
||||
import { Routes } from "@angular/router";
|
||||
import { DetailedCardComponent } from "app/components";
|
||||
|
||||
export const components: any[] = [
|
||||
DiscoverComponent,
|
||||
|
@ -18,6 +20,8 @@ export const components: any[] = [
|
|||
DiscoverActorComponent,
|
||||
DiscoverSearchResultsComponent,
|
||||
CarouselListComponent,
|
||||
RecentlyRequestedListComponent,
|
||||
DetailedCardComponent,
|
||||
];
|
||||
|
||||
export const providers: any[] = [
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<div *ngIf="requests$ | async as requests">
|
||||
<div *ngIf="requests.length > 0">
|
||||
<p-carousel #carousel [value]="requests" [numVisible]="3" [numScroll]="1"
|
||||
[responsiveOptions]="responsiveOptions" [page]="0">
|
||||
<ng-template let-result pTemplate="item">
|
||||
<ombi-detailed-card [request]="result" [isAdmin]="isAdmin" (onClick)="navigate(result)"
|
||||
(onApprove)="approve(result)"></ombi-detailed-card>
|
||||
</ng-template>
|
||||
</p-carousel>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,112 @@
|
|||
@import "~styles/variables.scss";
|
||||
|
||||
.ombi-card {
|
||||
padding: 5px;
|
||||
}
|
||||
::ng-deep .p-carousel-indicators {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
.image {
|
||||
border-radius: 10px;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
transition: .5s ease;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
.middle {
|
||||
transition: .5s ease;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 75%;
|
||||
width: 90%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
|
||||
.c {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c:hover .image {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.c:hover .middle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
.top-left {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
margin-top:-61px;
|
||||
}
|
||||
|
||||
@media (max-width:520px){
|
||||
.right{
|
||||
margin-top:0px;
|
||||
text-align: center;;
|
||||
}
|
||||
}
|
||||
|
||||
.discover-filter-buttons-group {
|
||||
background: $ombi-background-primary;
|
||||
border: 1px solid #293a4c;
|
||||
border-radius: 30px;
|
||||
color:#fff;
|
||||
margin-bottom:10px;
|
||||
margin-right: 30px;
|
||||
|
||||
.discover-filter-button{
|
||||
background:inherit;
|
||||
color:inherit;
|
||||
padding:0 0px;
|
||||
border-radius: 30px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
border-left:none;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content{
|
||||
line-height:40px;
|
||||
}
|
||||
|
||||
.button-active{
|
||||
background:#293a4c;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
::ng-deep .discover-filter-button .mat-button-toggle-button:focus{
|
||||
outline:none;
|
||||
}
|
||||
|
||||
.card-skeleton {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
@media (min-width:755px){
|
||||
::ng-deep .p-carousel-item{
|
||||
flex: 1 0 200px !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import { Component, OnInit, Input, ViewChild, OnDestroy } from "@angular/core";
|
||||
import { IRecentlyRequested, IRequestEngineResult, RequestType } from "../../../interfaces";
|
||||
import { Carousel } from 'primeng/carousel';
|
||||
import { ResponsiveOptions } from "../carousel.options";
|
||||
import { RequestServiceV2 } from "app/services/requestV2.service";
|
||||
import { finalize, map, Observable, Subject, takeUntil, tap } from "rxjs";
|
||||
import { Router } from "@angular/router";
|
||||
import { AuthService } from "app/auth/auth.service";
|
||||
import { NotificationService, RequestService } from "app/services";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
export enum DiscoverType {
|
||||
Upcoming,
|
||||
Trending,
|
||||
Popular,
|
||||
RecentlyRequested,
|
||||
Seasonal,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "ombi-recently-list",
|
||||
templateUrl: "./recently-requested-list.component.html",
|
||||
styleUrls: ["./recently-requested-list.component.scss"],
|
||||
})
|
||||
export class RecentlyRequestedListComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() public id: string;
|
||||
@Input() public isAdmin: boolean;
|
||||
@ViewChild('carousel', {static: false}) carousel: Carousel;
|
||||
|
||||
public requests$: Observable<IRecentlyRequested[]>;
|
||||
|
||||
public responsiveOptions: any;
|
||||
public RequestType = RequestType;
|
||||
public loadingFlag: boolean;
|
||||
public DiscoverType = DiscoverType;
|
||||
|
||||
private $loadSub = new Subject<void>();
|
||||
|
||||
constructor(private requestServiceV2: RequestServiceV2,
|
||||
private requestService: RequestService,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private notificationService: NotificationService,
|
||||
private translateService: TranslateService) {
|
||||
Carousel.prototype.onTouchMove = () => {};
|
||||
this.responsiveOptions = ResponsiveOptions;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.$loadSub.next();
|
||||
this.$loadSub.complete();
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.loadData();
|
||||
this.isAdmin = this.authService.isAdmin();
|
||||
}
|
||||
|
||||
public navigate(request: IRecentlyRequested) {
|
||||
this.router.navigate([this.generateDetailsLink(request), request.mediaId]);
|
||||
}
|
||||
|
||||
public approve(request: IRecentlyRequested) {
|
||||
switch(request.type) {
|
||||
case RequestType.movie:
|
||||
this.requestService.approveMovie({id: request.requestId, is4K: false}).pipe(
|
||||
map((res) => this.handleApproval(res, request))
|
||||
).subscribe();
|
||||
break;
|
||||
case RequestType.tvShow:
|
||||
this.requestService.approveChild({id: request.requestId}).pipe(
|
||||
tap((res) => this.handleApproval(res, request))
|
||||
).subscribe();
|
||||
break;
|
||||
case RequestType.album:
|
||||
this.requestService.approveAlbum({id: request.requestId}).pipe(
|
||||
tap((res) => this.handleApproval(res, request))
|
||||
).subscribe();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleApproval(result: IRequestEngineResult, request: IRecentlyRequested) {
|
||||
if (result.result) {
|
||||
this.notificationService.success(this.translateService.instant("Requests.SuccessfullyApproved"));
|
||||
request.approved = true;
|
||||
} else {
|
||||
this.notificationService.error(result.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private generateDetailsLink(request: IRecentlyRequested): string {
|
||||
switch (request.type) {
|
||||
case RequestType.movie:
|
||||
return `/details/movie/`;
|
||||
case RequestType.tvShow:
|
||||
return `/details/tv/`;
|
||||
case RequestType.album: //Actually artist
|
||||
return `/details/artist/`;
|
||||
}
|
||||
}
|
||||
|
||||
private loadData() {
|
||||
this.requests$ = this.requestServiceV2.getRecentlyRequested().pipe(
|
||||
tap(() => this.loading()),
|
||||
takeUntil(this.$loadSub),
|
||||
finalize(() => this.finishLoading())
|
||||
);
|
||||
}
|
||||
|
||||
private loading() {
|
||||
this.loadingFlag = true;
|
||||
}
|
||||
|
||||
private finishLoading() {
|
||||
this.loadingFlag = false;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import { ImageComponent } from 'app/components';
|
|||
MatButtonToggleModule,
|
||||
InfiniteScrollModule,
|
||||
SkeletonModule,
|
||||
ImageComponent
|
||||
ImageComponent,
|
||||
],
|
||||
declarations: [
|
||||
...fromComponents.components
|
||||
|
|
|
@ -107,3 +107,16 @@ export interface IPlexServerResponse {
|
|||
port: string;
|
||||
scheme: string;
|
||||
}
|
||||
|
||||
export interface IPlexWatchlistUsers {
|
||||
userId: string;
|
||||
syncStatus: WatchlistSyncStatus;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export enum WatchlistSyncStatus
|
||||
{
|
||||
Successful,
|
||||
Failed,
|
||||
NotEnabled
|
||||
}
|
19
src/Ombi/ClientApp/src/app/interfaces/IRecentlyRequested.ts
Normal file
19
src/Ombi/ClientApp/src/app/interfaces/IRecentlyRequested.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { RequestType } from "./IRequestModel";
|
||||
|
||||
export interface IRecentlyRequested {
|
||||
requestId: number;
|
||||
userId: string;
|
||||
username: string;
|
||||
available: boolean;
|
||||
tvPartiallyAvailable: boolean;
|
||||
requestDate: Date;
|
||||
title: string;
|
||||
overview: string;
|
||||
releaseDate: Date;
|
||||
approved: boolean;
|
||||
mediaId: string;
|
||||
type: RequestType;
|
||||
|
||||
posterPath: string;
|
||||
background: string;
|
||||
}
|
|
@ -142,7 +142,6 @@ export interface ISonarrSettings extends IExternalSettings {
|
|||
rootPathAnime: string;
|
||||
fullRootPath: string;
|
||||
addOnly: boolean;
|
||||
v3: boolean;
|
||||
languageProfile: number;
|
||||
languageProfileAnime: number;
|
||||
scanForAvailability: boolean;
|
||||
|
|
|
@ -21,3 +21,4 @@ export * from "./IVote";
|
|||
export * from "./IFailedRequests";
|
||||
export * from "./IHub";
|
||||
export * from "./ITester";
|
||||
export * from "./IRecentlyRequested";
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ArtistDetailsComponent } from "./artist/artist-details.component";
|
|||
import { ArtistInformationPanel } from "./artist/panels/artist-information-panel/artist-information-panel.component";
|
||||
import { ArtistReleasePanel } from "./artist/panels/artist-release-panel/artist-release-panel.component";
|
||||
import { CastCarouselComponent } from "./shared/cast-carousel/cast-carousel.component";
|
||||
import { CrewCarouselComponent } from "./shared/crew-carousel/crew-carousel.component";
|
||||
import { DenyDialogComponent } from "./shared/deny-dialog/deny-dialog.component";
|
||||
import { IssuesPanelComponent } from "./shared/issues-panel/issues-panel.component";
|
||||
import { MediaPosterComponent } from "./shared/media-poster/media-poster.component";
|
||||
|
@ -32,6 +33,7 @@ export const components: any[] = [
|
|||
SocialIconsComponent,
|
||||
MediaPosterComponent,
|
||||
CastCarouselComponent,
|
||||
CrewCarouselComponent,
|
||||
DenyDialogComponent,
|
||||
TvRequestsPanelComponent,
|
||||
MovieAdvancedOptionsComponent,
|
||||
|
|
|
@ -10,9 +10,8 @@
|
|||
|
||||
<social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id"
|
||||
[hasTrailer]="movie.videos?.results?.length > 0" [imdbId]="movie.imdbId"
|
||||
[twitter]="movie.externalIds.twitterId" [facebook]="movie.externalIds.facebookId"
|
||||
[instagram]="movie.externalIds.instagramId" [available]="movie.available" [plexUrl]="movie.plexUrl"
|
||||
[embyUrl]="movie.embyUrl" [jellyfinUrl]="movie.jellyfinUrl" [isAdmin]="isAdmin"
|
||||
[twitter]="movie.externalIds?.twitterId" [facebook]="movie.externalIds?.facebookId"
|
||||
[instagram]="movie.externalIds?.instagramId" [available]="movie.available" [isAdmin]="isAdmin"
|
||||
[canShowAdvanced]="showAdvanced && movieRequest" [type]="requestType" [has4KRequest]="movie.has4KRequest"
|
||||
(openTrailer)="openDialog()" (onAdvancedOptions)="openAdvancedOptions()"
|
||||
(onReProcessRequest)="reProcessRequest(false)" (onReProcess4KRequest)="reProcessRequest(true)">
|
||||
|
@ -205,6 +204,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<crew-carousel [crew]="movie.credits.crew"></crew-carousel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="row card-spacer" *ngIf="movie.videos?.results?.length > 0">
|
||||
|
||||
<div class="col-md-6" *ngFor="let video of movie.videos?.results">
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit, ViewEncapsulation } from "@angular/core";
|
|||
import { ImageService, SearchV2Service, RequestService, MessageService, RadarrService, SettingsStateService } from "../../../services";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { DomSanitizer } from "@angular/platform-browser";
|
||||
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
|
||||
import { ICrewViewModel, ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component";
|
||||
import { AuthService } from "../../../auth/auth.service";
|
||||
|
@ -82,6 +82,7 @@ export class MovieDetailsComponent implements OnInit{
|
|||
this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => {
|
||||
this.movie = x;
|
||||
this.checkPoster();
|
||||
this.movie.credits.crew = this.orderCrew(this.movie.credits.crew);
|
||||
if (this.movie.requestId > 0) {
|
||||
// Load up this request
|
||||
this.hasRequest = true;
|
||||
|
@ -93,6 +94,7 @@ export class MovieDetailsComponent implements OnInit{
|
|||
this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(async x => {
|
||||
this.movie = x;
|
||||
this.checkPoster();
|
||||
this.movie.credits.crew = this.orderCrew(this.movie.credits.crew);
|
||||
if (this.movie.requestId > 0) {
|
||||
// Load up this request
|
||||
this.hasRequest = true;
|
||||
|
@ -319,4 +321,16 @@ export class MovieDetailsComponent implements OnInit{
|
|||
|
||||
});
|
||||
}
|
||||
|
||||
private orderCrew(crew: ICrewViewModel[]): ICrewViewModel[] {
|
||||
return crew.sort((a, b) => {
|
||||
if (a.job === "Director") {
|
||||
return -1;
|
||||
} else if (b.job === "Director") {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<a *ngIf="item.profile_path" [routerLink]="'/discover/actor/' + item.id">
|
||||
<ombi-image class="cast-profile-img" src="https://image.tmdb.org/t/p/w300{{item.profile_path}}"></ombi-image>
|
||||
</a>
|
||||
<!-- TODO get profile image default -->
|
||||
<i *ngIf="!item.image && !item.profile_path" class="fa-solid fa-user-large fa-2xl crew-profile-img" aria-hidden="true"></i>
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<mat-card class="mat-elevation-z8 spacing-below">
|
||||
<mat-card-header>{{'MediaDetails.Crews.CrewTitle' | translate}}</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p-carousel [value]="crew" easing="easeOutStrong" [responsiveOptions]="responsiveOptions" [numVisible]="5" >
|
||||
<ng-template let-item pTemplate="item">
|
||||
<div class="row justify-content-md-center mat-card mat-card-flat carousel-item">
|
||||
<div [routerLink]="'/discover/actor/' + item.id" class="bottom-space link">
|
||||
<a *ngIf="item.image">
|
||||
<img class="crew-profile-img" src="https://image.tmdb.org/t/p/w300{{item.image}}">
|
||||
</a>
|
||||
<a *ngIf="item.profile_path">
|
||||
<img class="crew-profile-img" src="https://image.tmdb.org/t/p/w300{{item.profile_path}}">
|
||||
</a>
|
||||
<i *ngIf="!item.image && !item.profile_path" class="fa-solid fa-user-large fa-2xl crew-profile-img" aria-hidden="true"></i>
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<span *ngIf="item.name"><strong>{{item.name}}</strong></span>
|
||||
<span *ngIf="item.person"><strong>{{item.person}}</strong></span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<span *ngIf="item.job"><small>{{item.job}}</small></span>
|
||||
<span *ngIf="item.department"><small>{{item.position}}</small></span>
|
||||
<span *ngIf="item.title"><small>{{item.title}}</small></span>
|
||||
<span *ngIf="item.overview"><small>{{item.overview}}</small></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-carousel>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,87 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "~styles/variables.scss";
|
||||
::ng-deep body .ui-carousel .ui-carousel-content .ui-carousel-prev,
|
||||
body .ui-carousel .ui-carousel-content .ui-carousel-next {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px rgba(178, 193, 205, 0.64);
|
||||
-moz-border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
margin: 0.2em;
|
||||
color: #333333;
|
||||
-moz-transition: color 0.2s;
|
||||
-o-transition: color 0.2s;
|
||||
-webkit-transition: color 0.2s;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
::ng-deep body .ui-carousel .ui-carousel-content .ui-carousel-prev:not(.ui-state-disabled):hover,
|
||||
body .ui-carousel .ui-carousel-content .ui-carousel-next:not(.ui-state-disabled):hover {
|
||||
background-color: #ffffff;
|
||||
color: #000;
|
||||
border-color: solid 1px rgba(178, 193, 205, 0.64);
|
||||
}
|
||||
|
||||
::ng-deep body .ui-carousel .ui-carousel-dots-container .ui-carousel-dot-item>.ui-button {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::ng-deep body .ui-carousel .ui-carousel-dots-container .ui-carousel-dot-item .ui-carousel-dot-icon {
|
||||
width: 20px;
|
||||
height: 6px;
|
||||
background-color: rgba(255, 255, 255, 0.44);
|
||||
margin: 0 0.2em;
|
||||
}
|
||||
|
||||
::ng-deep body .ui-carousel .ui-carousel-dots-container .ui-carousel-dot-item .ui-carousel-dot-icon::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
::ng-deep body .ui-carousel .ui-carousel-dots-container .ui-carousel-dot-item.ui-state-highlight .ui-carousel-dot-icon {
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::ng-deep .ui-carousel-next {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px rgba(178, 193, 205, 0.64);
|
||||
border-radius: 50%;
|
||||
margin: 0.2em;
|
||||
color: #333333;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
::ng-deep .ui-carousel-content button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 979px) {
|
||||
|
||||
.crew-profile-img {
|
||||
border-radius: 100%;
|
||||
width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
@media (max-width: 978px) {
|
||||
|
||||
.crew-profile-img {
|
||||
border-radius: 100%;
|
||||
width: 100px;
|
||||
max-height: 100px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { Component, Input } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "crew-carousel",
|
||||
templateUrl: "./crew-carousel.component.html",
|
||||
styleUrls: ["./crew-carousel.component.scss"]
|
||||
})
|
||||
export class CrewCarouselComponent {
|
||||
|
||||
constructor() {
|
||||
this.responsiveOptions = [
|
||||
{
|
||||
breakpoint: '1024px',
|
||||
numVisible: 5,
|
||||
numScroll: 5
|
||||
},
|
||||
{
|
||||
breakpoint: '768px',
|
||||
numVisible: 3,
|
||||
numScroll: 3
|
||||
},
|
||||
{
|
||||
breakpoint: '560px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@Input() crew: any[];
|
||||
public responsiveOptions: any[];
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue