Merge branch 'Ombi-app:develop' into develop

This commit is contained in:
dtalens 2022-10-03 00:57:19 +02:00 committed by GitHub
commit fc7d087a8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
159 changed files with 9605 additions and 4384 deletions

View file

@ -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

View file

@ -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:

View file

@ -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))

View file

@ -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
View 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/

View file

@ -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" />

View file

@ -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}")
{
}
}
}
}

View file

@ -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; }
}

View file

@ -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>();
}

View file

@ -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; }
}
}

View file

@ -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)

View file

@ -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; }
}
}

View file

@ -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))
{

View 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));
});
}
}
}

View 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)));
}
}
}

View file

@ -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
};

View file

@ -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);

View file

@ -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(),
};
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}

View 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
}
}

View 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; }
}
}

View file

@ -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!

View 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);
}
}

View 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);
}
}

View 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;
}
}
}

View 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();
}
}
}

View file

@ -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)

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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";

View file

@ -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>

View file

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

View file

@ -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 },
};
}
}

View 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);
}
}

View file

@ -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>

View file

@ -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);
}
}
}

View 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);
}
}
}

View file

@ -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
{

View file

@ -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;

View 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);
}
}
}
}

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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...

View file

@ -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,

View file

@ -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}");

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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; }

View file

@ -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,

View file

@ -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,

View file

@ -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; }

View 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; }
}
}

View file

@ -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; }

File diff suppressed because it is too large Load diff

View file

@ -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");
}
}
}

View file

@ -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")

File diff suppressed because it is too large Load diff

View file

@ -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");
}
}
}

View file

@ -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")

View 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;
}
}
}

View file

@ -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);
}
}

View 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; }
}
}

View file

@ -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;

View file

@ -22,7 +22,8 @@
"emby",
"availability-rules",
"details",
"requests"
"requests",
"sonarr"
],
"rpc.enabled": true
}

View file

@ -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,
}
}

View file

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

View file

@ -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 = {

View file

@ -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",

View file

@ -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',
};

View file

@ -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;
}

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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,
};

View file

@ -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;
}
}
}

View file

@ -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"/>

View file

@ -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);
}
}

View file

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

View file

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

View file

@ -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() {

View file

@ -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
}
];

View file

@ -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>

View file

@ -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[] = [

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -19,7 +19,7 @@ import { ImageComponent } from 'app/components';
MatButtonToggleModule,
InfiniteScrollModule,
SkeletonModule,
ImageComponent
ImageComponent,
],
declarations: [
...fromComponents.components

View file

@ -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
}

View 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;
}

View file

@ -142,7 +142,6 @@ export interface ISonarrSettings extends IExternalSettings {
rootPathAnime: string;
fullRootPath: string;
addOnly: boolean;
v3: boolean;
languageProfile: number;
languageProfileAnime: number;
scanForAvailability: boolean;

View file

@ -21,3 +21,4 @@ export * from "./IVote";
export * from "./IFailedRequests";
export * from "./IHub";
export * from "./ITester";
export * from "./IRecentlyRequested";

View file

@ -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,

View file

@ -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">

View file

@ -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;
}
});
}
}

View file

@ -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">

View file

@ -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>

View file

@ -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;
}

View file

@ -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