diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 2684ccc3e..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,48 +0,0 @@ - - -#### Ombi build Version: - -V 3.0.XX - -#### Update Branch: - -Open Beta - -#### Media Sever: - -Plex/Emby - -#### Media Server Version: - - - -#### Operating System: - -(Place text here) - - -#### Ombi Applicable Logs (from `/logs/` directory or the Admin page): - -``` - -(Logs go here. Don't remove the ' tags for showing your logs correctly. Please make sure you remove any personal information from the logs) - -``` - -#### Problem Description: - -(Place text here) - -#### Reproduction Steps: - -Please include any steps to reproduce the issue, this the request that is causing the problem etc. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..2236cc395 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Logs (Logs directory where Ombi is located)** +If applicable, a snippet of the logs that seems relevant to the bug if present. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + +**Ombi Version (please complete the following information):** + - Version [e.g. 3.0.1158] +- Media Server [e.g. Plex] + +**Additional context** +Add any other context about the problem here. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7ea6f21..5f2948060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,105 @@ # Changelog -## (unreleased) +## v3.0.4256 (2019-02-18) ### **New Features** +- Update discord link to follow the scheme of other links. [Tom McClellan] + +- Update issue templates. [Jamie] + +- Update README.md. [Jamie] + +- Update CHANGELOG.md. [Jamie] + +- Added the functionality to remove a user from getting notifications to their mobile device #2780. [tidusjar] + +- Added a demo mode, this will only show movies and shows that are in the public domain. Dam that stupid fruit company. [tidusjar] + +- Added Actor Searching for Movies! [TidusJar] + +- Added the ability to change where the View on Emby link goes to #2730. [TidusJar] + +- Added the request queue to the notifications UI so you can turn it off per notification agent #2747. [TidusJar] + - Added new classes to the posters #2732. [TidusJar] +### **Fixes** + +- Fix: src/Ombi/package.json to reduce vulnerabilities. [snyk-bot] + +- Fixed #2801 this is when a season is not correctly monitored in sonarr when approved by an admin. [tidusjar] + +- Small improvements to try and mitigate #2750. [tidusjar] + +- Removed some areas where we clear out the cache. This should help with DB locking #2750. [tidusjar] + +- Fixed #2810. [tidusjar] + +- Cannot create an issue comment with the API #2811. [tidusjar] + +- Set the local domain if the Application URL is set for the HELO or EHLO commands. #2636. [tidusjar] + +- New translations en.json (Spanish) [Jamie] + +- Delete ISSUE_TEMPLATE.md. [Jamie] + +- More minor grammatical edits. [Andrew Metzger] + +- Minor grammatical edits. [Andrew Metzger] + +- Fixed #2802 the issue where "Issues" were not being deleted correctly. [tidusjar] + +- Fixed #2797. [tidusjar] + +- New translations en.json (Dutch) [Jamie] + +- New translations en.json (Spanish) [Jamie] + +- New translations en.json (Portuguese, Brazilian) [Jamie] + +- Fixed #2786. [tidusjar] + +- Fixed #2756. [tidusjar] + +- Ignore the UserName header as part of the Api is the value is an empty string. [tidusjar] + +- Fixed #2759. [tidusjar] + +- Did #2756. [TidusJar] + +- Fixed the exception that sometimes makes ombi fallover. [TidusJar] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- New translations en.json (Swedish) [Jamie] + +- Log the error to the ui to figure out what's going on with #2755. [tidusjar] + +- Small fix when denying a request with a reason, we wasn't updating the ui. [TidusJar] + +- Make sure we can only set the ApiAlias when using the API Key. [tidusjar] + +- #2363 Added the ability to pass any username into the API using the ApiAlias header. [tidusjar] + +- Removed the Add user to Plex from Ombi. [tidusjar] + ## v3.0.4119 (2019-01-09) diff --git a/README.md b/README.md index 6347a8a5f..0af908f65 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ We integrate with the following applications: Supported notifications: * SMTP Notifications (Email) * Discord +* Gotify * Slack * Pushbullet * Pushover @@ -117,13 +118,12 @@ Please feel free to submit a pull request! # Donation If you feel like donating you can donate with the below buttons! -[![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi) -[![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet) + +[![Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://patreon.com/tidusjar/Ombi) +[![Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/PlexRequestsNet) ### A massive thanks to everyone for all their help! -## Stats -[![Throughput Graph](https://graphs.waffle.io/tidusjar/PlexRequests.Net/throughput.svg)](https://waffle.io/tidusjar/PlexRequests.Net/metrics/throughput) ### Sponsors ### - [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools diff --git a/appveyor.yml b/appveyor.yml index 3c60a0006..0d5591971 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,17 @@ -version: 3.0.{build} +version: 4.0.{build} configuration: Release os: Visual Studio 2017 + environment: - nodejs_version: "9.8.0" + nodejs_version: "11.5.0" typescript_version: "3.0.1" github_auth_token: secure: H/7uCrjmWHGJxgN3l9fbhhdVjvvWI8VVF4ZzQqeXuJwAf+PgSNBdxv4SS+rMQ+RH - sonarrcloudtoken: - secure: WGkIog4wuMSx1q5vmSOgIBXMtI/leMpLbZhi9MJnJdBBuDfcv12zwXg3LQwY0WbE + + + +# Do not build on tags (GitHub and BitBucket) +skip_tags: true install: # Get the latest stable version of Node.js or io.js @@ -16,35 +20,45 @@ install: - cmd: set path=%programfiles(x86)%\\Microsoft SDKs\TypeScript\3.0;%path% - cmd: tsc -v build_script: - # - dotnet tool install --global dotnet-sonarscanner - #- ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER) { dotnet sonarscanner begin /k:"tidusjar_Ombi" /d:sonar.organization="tidusjar-github" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.login="$env.sonarrcloud_token" /d:sonar.analysis.mode="preview" /d:sonar.github.pullRequest="$env:APPVEYOR_PULL_REQUEST_NUMBER" /d:sonar.github.repository="https://github.com/tidusjar/ombi" /d:sonar.github.oauth="$env.github_auth_token" } - # - ps: if (-Not $env:APPVEYOR_PULL_REQUEST_NUMBER) { dotnet sonarscanner begin /k:"tidusjar_Ombi" /d:sonar.organization="tidusjar-github" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.login="$env.SONARRCLOUDTOKEN" } - - ps: ./build.ps1 --settings_skipverification=true - # - dotnet sonarscanner end /d:sonar.login="%sonarrcloudtoken%" + - ps: | + $deployBranches = + "feature/v4", + "develop", + "master"; -test: off + If(($env:APPVEYOR_REPO_BRANCH -in $deployBranches -Or $env:APPVEYOR_REPO_COMMIT_MESSAGE -Match '!deploy') -And $env:APPVEYOR_REPO_COMMIT_MESSAGE -NotMatch '!build') { + Write-Output "This is a deployment build" + $env:Deploy = 'true' + ./build.ps1 + } + Else + { + $env:Deploy = 'false' + Write-Output "This is a not a deployment build" + ./build.ps1 --target=build + } +skip_commits: + files: + - '**/*.md' + after_build: -- cmd: >- - - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\windows.zip" - - - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\osx.tar.gz" - - - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\linux.tar.gz" +- ps: | + $deployBranches = + "feature/v4", + "develop", + "master"; - - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\linux-arm.tar.gz" - - - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\windows-32bit.zip" - - - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\linux-arm64.tar.gz" - - + If(($env:APPVEYOR_REPO_BRANCH -in $deployBranches -Or $env:APPVEYOR_REPO_COMMIT_MESSAGE -Match '!deploy') -And $env:APPVEYOR_REPO_COMMIT_MESSAGE -NotMatch '!build') + { + Write-Output "Deploying!" + Get-ChildItem -Recurse .\*.zip | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + Get-ChildItem -Recurse .\*.gz | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + } + Else + { + Write-Output "No Deployment" + } #cache: #- '%USERPROFILE%\.nuget\packages' diff --git a/build.cake b/build.cake index 080ef167e..fb72fb839 100644 --- a/build.cake +++ b/build.cake @@ -1,10 +1,10 @@ -#tool "nuget:?package=GitVersion.CommandLine" -#addin "Cake.Gulp" -#addin "SharpZipLib" -#addin nuget:?package=Cake.Compression&version=0.1.4 -#addin "Cake.Incubator" -#addin "Cake.Yarn" +#tool "nuget:?package=GitVersion.CommandLine&version=4.0.0" +#addin nuget:?package=SharpZipLib&version=1.1.0 +#addin nuget:?package=Cake.Compression&version=0.2.2 +#addin "Cake.Incubator&version=3.1.0" +#addin nuget:?package=Cake.Yarn&version=0.4.5 +#addin "Cake.Powershell" ////////////////////////////////////////////////////////////////////// // ARGUMENTS @@ -82,9 +82,9 @@ Task("SetVersionInfo") versionInfo = GitVersion(settings); - Information("GitResults -> {0}", versionInfo.Dump()); +// Information("GitResults -> {0}", versionInfo.Dump()); - Information(@"Build:{0}",AppVeyor.Environment.Build.Dump()); +//Information(@"Build:{0}",AppVeyor.Environment.Build.Dump()); var buildVersion = string.Empty; if(string.IsNullOrEmpty(AppVeyor.Environment.Build.Version)) @@ -135,7 +135,7 @@ Task("Gulp Publish") Task("TSLint") .Does(() => { - // Yarn.FromPath(uiProjectDir).RunScript("lint"); + //Yarn.FromPath(uiProjectDir).RunScript("lint"); }); Task("PrePublish") @@ -156,6 +156,7 @@ Task("Package") }); Task("Publish") + .IsDependentOn("Run-Unit-Tests") .IsDependentOn("PrePublish") .IsDependentOn("Publish-Windows") .IsDependentOn("Publish-Windows-32bit") @@ -250,9 +251,43 @@ Task("Publish-Linux-ARM-64Bit") Task("Run-Unit-Tests") .Does(() => -{ - DotNetCoreBuild(csProj, buildSettings); +{ + var settings = new DotNetCoreTestSettings + { + ArgumentCustomization = args => args.Append("--logger \"trx;LogFileName=Test.trx\""), + Configuration = "Release" + }; + var projectFiles = GetFiles("./**/*Tests.csproj"); + foreach(var file in projectFiles) + { + DotNetCoreTest(file.FullPath, settings); + } + + var script = @" + $wc = New-Object 'System.Net.WebClient' + foreach ($name in Resolve-Path .\src\**\TestResults\Test*.trx) + { + $wc.UploadFile(""https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)\"", $name) + } +"; + // Upload the results + StartPowershellScript(script); }); + +Task("Run-Server-Build") + .Does(() => + { + var settings = new DotNetCoreBuildSettings + { + Framework = frameworkVer, + Configuration = "Release", + OutputDirectory = Directory(buildDir) + }; + DotNetCoreBuild(csProj, settings); + }); + +Task("Run-UI-Build") + .IsDependentOn("PrePublish"); ////////////////////////////////////////////////////////////////////// // TASK TARGETS ////////////////////////////////////////////////////////////////////// @@ -260,6 +295,12 @@ Task("Run-Unit-Tests") Task("Default") .IsDependentOn("Publish"); +Task("Build") + .IsDependentOn("SetVersionInfo") + .IsDependentOn("Run-Unit-Tests") + .IsDependentOn("Run-Server-Build"); + // .IsDependentOn("Run-UI-Build"); + ////////////////////////////////////////////////////////////////////// // EXECUTION ////////////////////////////////////////////////////////////////////// diff --git a/build.ps1 b/build.ps1 index 3a8ef5c4c..e61281292 100644 --- a/build.ps1 +++ b/build.ps1 @@ -21,40 +21,49 @@ The build script target to run. The build configuration to use. .PARAMETER Verbosity Specifies the amount of information to be displayed. -.PARAMETER Experimental -Tells Cake to use the latest Roslyn release. -.PARAMETER WhatIf -Performs a dry run of the build script. -No tasks will be executed. -.PARAMETER Mono -Tells Cake to use the Mono scripting engine. +.PARAMETER ShowDescription +Shows description about tasks. +.PARAMETER DryRun +Performs a dry run. .PARAMETER SkipToolPackageRestore Skips restoring of packages. .PARAMETER ScriptArgs Remaining arguments are added here. .LINK -http://cakebuild.net +https://cakebuild.net #> [CmdletBinding()] Param( [string]$Script = "build.cake", - [string]$Target = "Default", - [ValidateSet("Release", "Debug")] - [string]$Configuration = "Release", + [string]$Target, + [string]$Configuration, [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [Alias("DryRun","Noop")] - [switch]$WhatIf, - [switch]$Mono, + [string]$Verbosity, + [switch]$ShowDescription, + [Alias("WhatIf", "Noop")] + [switch]$DryRun, [switch]$SkipToolPackageRestore, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$ScriptArgs ) +# Attempt to set highest encryption available for SecurityProtocol. +# PowerShell will not set this by default (until maybe .NET 4.6.x). This +# will typically produce a message for PowerShell v2 (just an info +# message though) +try { + # Set TLS 1.2 (3072), then TLS 1.1 (768), then TLS 1.0 (192), finally SSL 3.0 (48) + # Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't + # exist in .NET 4.0, even though they are addressable if .NET 4.5+ is + # installed (.NET 4.5 is an in-place upgrade). + [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 + } catch { + Write-Output 'Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to upgrade to .NET Framework 4.5+ and PowerShell v3' + } + [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null function MD5HashFile([string] $filePath) { @@ -80,6 +89,15 @@ function MD5HashFile([string] $filePath) } } +function GetProxyEnabledWebClient +{ + $wc = New-Object System.Net.WebClient + $proxy = [System.Net.WebRequest]::GetSystemWebProxy() + $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials + $wc.Proxy = $proxy + return $wc +} + Write-Host "Preparing to run build script..." if(!$PSScriptRoot){ @@ -87,31 +105,15 @@ if(!$PSScriptRoot){ } $TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" +$MODULES_DIR = Join-Path $TOOLS_DIR "Modules" $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" - -# Should we use mono? -$UseMono = ""; -if($Mono.IsPresent) { - Write-Verbose -Message "Using the Mono based scripting engine." - $UseMono = "-mono" -} - -# Should we use the new Roslyn? -$UseExperimental = ""; -if($Experimental.IsPresent -and !($Mono.IsPresent)) { - Write-Verbose -Message "Using experimental version of Roslyn." - $UseExperimental = "-experimental" -} - -# Is this a dry run? -$UseDryRun = ""; -if($WhatIf.IsPresent) { - $UseDryRun = "-dryrun" -} +$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" +$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" # Make sure tools folder exists if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { @@ -122,7 +124,10 @@ if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { # Make sure that packages.config exist. if (!(Test-Path $PACKAGES_CONFIG)) { Write-Verbose -Message "Downloading packages.config..." - try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + try { + $wc = GetProxyEnabledWebClient + $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) + } catch { Throw "Could not download packages.config." } } @@ -142,7 +147,8 @@ if (!(Test-Path $NUGET_EXE)) { if (!(Test-Path $NUGET_EXE)) { Write-Verbose -Message "Downloading NuGet.exe..." try { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + $wc = GetProxyEnabledWebClient + $wc.DownloadFile($NUGET_URL, $NUGET_EXE) } catch { Throw "Could not download NuGet.exe." } @@ -161,20 +167,56 @@ if(-Not $SkipToolPackageRestore.IsPresent) { if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { Write-Verbose -Message "Missing or changed package.config hash..." - Remove-Item * -Recurse -Exclude packages.config,nuget.exe + Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | + Remove-Item -Recurse } Write-Verbose -Message "Restoring tools from NuGet..." $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet tools." + Throw "An error occurred while restoring NuGet tools." } else { $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" } Write-Verbose -Message ($NuGetOutput | out-string) + + Pop-Location +} + +# Restore addins from NuGet +if (Test-Path $ADDINS_PACKAGES_CONFIG) { + Push-Location + Set-Location $ADDINS_DIR + + Write-Verbose -Message "Restoring addins from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred while restoring NuGet addins." + } + + Write-Verbose -Message ($NuGetOutput | out-string) + + Pop-Location +} + +# Restore modules from NuGet +if (Test-Path $MODULES_PACKAGES_CONFIG) { + Push-Location + Set-Location $MODULES_DIR + + Write-Verbose -Message "Restoring modules from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred while restoring NuGet modules." + } + + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location } @@ -183,7 +225,18 @@ if (!(Test-Path $CAKE_EXE)) { Throw "Could not find Cake.exe at $CAKE_EXE" } + + +# Build Cake arguments +$cakeArguments = @("$Script"); +if ($Target) { $cakeArguments += "-target=$Target" } +if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } +if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } +if ($ShowDescription) { $cakeArguments += "-showdescription" } +if ($DryRun) { $cakeArguments += "-dryrun" } +$cakeArguments += $ScriptArgs + # Start Cake Write-Host "Running build script..." -Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +&$CAKE_EXE $cakeArguments exit $LASTEXITCODE \ No newline at end of file diff --git a/src/Ombi.Api.Gotify/GotifyApi.cs b/src/Ombi.Api.Gotify/GotifyApi.cs new file mode 100644 index 000000000..8cd79a689 --- /dev/null +++ b/src/Ombi.Api.Gotify/GotifyApi.cs @@ -0,0 +1,36 @@ +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api.Gotify +{ + public class GotifyApi : IGotifyApi + { + public GotifyApi(IApi api) + { + _api = api; + } + + private readonly IApi _api; + + public async Task PushAsync(string baseUrl, string accessToken, string subject, string body, sbyte priority) + { + var request = new Request("/message", baseUrl, HttpMethod.Post); + request.AddQueryString("token", accessToken); + + request.AddHeader("Access-Token", accessToken); + request.ApplicationJsonContentType(); + + + var jsonBody = new + { + message = body, + title = subject, + priority = priority + }; + + request.AddJsonBody(jsonBody); + + await _api.Request(request); + } + } +} diff --git a/src/Ombi.Api.Gotify/IGotifyApi.cs b/src/Ombi.Api.Gotify/IGotifyApi.cs new file mode 100644 index 000000000..e6a6b4060 --- /dev/null +++ b/src/Ombi.Api.Gotify/IGotifyApi.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Api.Gotify +{ + public interface IGotifyApi + { + Task PushAsync(string endpoint, string accessToken, string subject, string body, sbyte priority); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Gotify/Ombi.Api.Gotify.csproj b/src/Ombi.Api.Gotify/Ombi.Api.Gotify.csproj new file mode 100644 index 000000000..ce5475fae --- /dev/null +++ b/src/Ombi.Api.Gotify/Ombi.Api.Gotify.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + 3.0.0.0 + 3.0.0.0 + + + + + + + + + diff --git a/src/Ombi.Api.Trakt/ITraktApi.cs b/src/Ombi.Api.Trakt/ITraktApi.cs index 17c4203cb..087784e01 100644 --- a/src/Ombi.Api.Trakt/ITraktApi.cs +++ b/src/Ombi.Api.Trakt/ITraktApi.cs @@ -12,5 +12,6 @@ namespace Ombi.Api.Trakt Task> GetMostWatchesShows(TraktTimePeriod period = null, int? page = default(int?), int? limitPerPage = default(int?)); Task> GetPopularShows(int? page = default(int?), int? limitPerPage = default(int?)); Task> GetTrendingShows(int? page = default(int?), int? limitPerPage = default(int?)); + Task GetTvExtendedInfo(string imdbId); } } \ No newline at end of file diff --git a/src/Ombi.Api.Trakt/TraktApi.cs b/src/Ombi.Api.Trakt/TraktApi.cs index 23773eb87..2e09ec071 100644 --- a/src/Ombi.Api.Trakt/TraktApi.cs +++ b/src/Ombi.Api.Trakt/TraktApi.cs @@ -23,7 +23,7 @@ namespace Ombi.Api.Trakt public async Task> GetPopularShows(int? page = null, int? limitPerPage = null) { - var popular = await Client.Shows.GetPopularShowsAsync(new TraktExtendedInfo { Full = true, Images = true}, null, page ?? 1, limitPerPage ?? 10); + var popular = await Client.Shows.GetPopularShowsAsync(new TraktExtendedInfo { Full = true, Images = true }, null, page ?? 1, limitPerPage ?? 10); return popular.Value; } @@ -44,6 +44,11 @@ namespace Ombi.Api.Trakt var anticipatedShows = await Client.Shows.GetMostWatchedShowsAsync(period ?? TraktTimePeriod.Monthly, new TraktExtendedInfo { Full = true, Images = true }, null, page ?? 1, limitPerPage ?? 10); return anticipatedShows.Value; } + + public async Task GetTvExtendedInfo(string imdbId) + { + return await Client.Shows.GetShowAsync(imdbId, new TraktExtendedInfo { Full = true }); + } } } diff --git a/src/Ombi.Api.TvMaze/ITvMazeApi.cs b/src/Ombi.Api.TvMaze/ITvMazeApi.cs index 9a8688568..819051d83 100644 --- a/src/Ombi.Api.TvMaze/ITvMazeApi.cs +++ b/src/Ombi.Api.TvMaze/ITvMazeApi.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Ombi.Api.TvMaze.Models; +using Ombi.Api.TvMaze.Models.V2; namespace Ombi.Api.TvMaze { @@ -11,5 +12,6 @@ namespace Ombi.Api.TvMaze Task> Search(string searchTerm); Task ShowLookup(int showId); Task ShowLookupByTheTvDbId(int theTvDbId); + Task GetTvFullInformation(int id); } } \ No newline at end of file diff --git a/src/Ombi.Api.TvMaze/Models/V2/FullSearch.cs b/src/Ombi.Api.TvMaze/Models/V2/FullSearch.cs new file mode 100644 index 000000000..77b9c4a28 --- /dev/null +++ b/src/Ombi.Api.TvMaze/Models/V2/FullSearch.cs @@ -0,0 +1,144 @@ +using System; + +namespace Ombi.Api.TvMaze.Models.V2 +{ + public class FullSearch + { + public int id { get; set; } + public string url { get; set; } + public string name { get; set; } + public string type { get; set; } + public string language { get; set; } + public string[] genres { get; set; } + public string status { get; set; } + public int runtime { get; set; } + public string premiered { get; set; } + public string officialSite { get; set; } + public Schedule schedule { get; set; } + public Rating rating { get; set; } + public int weight { get; set; } + public Network network { get; set; } + public object webChannel { get; set; } + public Externals externals { get; set; } + public Image image { get; set; } + public string summary { get; set; } + public int updated { get; set; } + public _Links _links { get; set; } + public _Embedded _embedded { get; set; } + } + + public class Schedule + { + public string time { get; set; } + public string[] days { get; set; } + } + + public class Rating + { + public float average { get; set; } + } + + public class Network + { + public int id { get; set; } + public string name { get; set; } + public Country country { get; set; } + } + + public class Country + { + public string name { get; set; } + public string code { get; set; } + public string timezone { get; set; } + } + + public class Externals + { + public int tvrage { get; set; } + public int thetvdb { get; set; } + public string imdb { get; set; } + } + + public class Image + { + public string medium { get; set; } + public string original { get; set; } + } + + public class _Links + { + public Self self { get; set; } + public Previousepisode previousepisode { get; set; } + } + + public class Self + { + public string href { get; set; } + } + + public class Previousepisode + { + public string href { get; set; } + } + + public class _Embedded + { + public Cast[] cast { get; set; } + public Crew[] crew { get; set; } + public Episode[] episodes { get; set; } + } + + public class Cast + { + public Person person { get; set; } + public Character character { get; set; } + public bool self { get; set; } + public bool voice { get; set; } + } + + public class Person + { + public int id { get; set; } + public string url { get; set; } + public string name { get; set; } + public Country country { get; set; } + public string birthday { get; set; } + public object deathday { get; set; } + public string gender { get; set; } + public Image image { get; set; } + public _Links _links { get; set; } + } + + + public class Character + { + public int id { get; set; } + public string url { get; set; } + public string name { get; set; } + public Image image { get; set; } + public _Links _links { get; set; } + } + + public class Crew + { + public string type { get; set; } + public Person person { get; set; } + } + + public class Episode + { + public int id { get; set; } + public string url { get; set; } + public string name { get; set; } + public int season { get; set; } + public int number { get; set; } + public string airdate { get; set; } + public string airtime { get; set; } + public DateTime airstamp { get; set; } + public int runtime { get; set; } + public Image image { get; set; } + public string summary { get; set; } + public _Links _links { get; set; } + } + +} diff --git a/src/Ombi.Api.TvMaze/TvMazeApi.cs b/src/Ombi.Api.TvMaze/TvMazeApi.cs index 9aa547483..5d761da47 100644 --- a/src/Ombi.Api.TvMaze/TvMazeApi.cs +++ b/src/Ombi.Api.TvMaze/TvMazeApi.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Ombi.Api.TvMaze.Models; +using Ombi.Api.TvMaze.Models.V2; using Ombi.Helpers; namespace Ombi.Api.TvMaze @@ -15,7 +15,6 @@ namespace Ombi.Api.TvMaze { Api = api; Logger = logger; - //Mapper = mapper; } private string Uri = "http://api.tvmaze.com"; private IApi Api { get; } @@ -75,5 +74,17 @@ namespace Ombi.Api.TvMaze return await Api.Request>(request); } + public async Task GetTvFullInformation(int id) + { + var request = new Request($"shows/{id}", Uri, HttpMethod.Get); + + request.AddQueryString("embed[]", "cast"); + request.AddQueryString("embed[]", "crew"); + request.AddQueryString("embed[]", "episodes"); + + request.AddContentHeader("Content-Type", "application/json"); + + return await Api.Request(request); + } } } diff --git a/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs b/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs new file mode 100644 index 000000000..bd6e8d5a2 --- /dev/null +++ b/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs @@ -0,0 +1,106 @@ +using Microsoft.AspNetCore.Identity; +using Moq; +using NUnit.Framework; +using Ombi.Api.Plex; +using Ombi.Api.Plex.Models; +using Ombi.Core.Authentication; +using Ombi.Core.Settings; +using Ombi.Settings.Settings.Models; +using Ombi.Store.Entities; +using Ombi.Test.Common; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Core.Tests.Authentication +{ + [TestFixture] + public class OmbiUserManagerTests + { + + [SetUp] + public void Setup() + { + + UserStore = new Mock>(); + PlexApi = new Mock(); + AuthenticationSettings = new Mock>(); + + AuthenticationSettings.Setup(x => x.GetSettingsAsync()) + .ReturnsAsync(new AuthenticationSettings()); + _um = new OmbiUserManager(UserStore.Object, null, null, null, null, null, null, null, null, + PlexApi.Object, null, null, AuthenticationSettings.Object); + } + + public OmbiUserManager _um { get; set; } + private Mock> UserStore { get; set; } + private Mock PlexApi { get; set; } + private Mock> AuthenticationSettings { get; set; } + + [Test] + public async Task CheckPassword_PlexUser_EmailLogin_ValidPassword() + { + var user = new OmbiUser + { + UserType = UserType.PlexUser, + EmailLogin = true, + Email = "MyEmail@email.com" + }; + PlexApi.Setup(x => x.SignIn(It.IsAny())) + .ReturnsAsync(new PlexAuthentication + { + user = new User + { + authentication_token = "abc" + } + }); + var result = await _um.CheckPasswordAsync(user, "pass"); + + Assert.That(result, Is.True); + PlexApi.Verify(x => x.SignIn(It.Is(c => c.login == "MyEmail@email.com")), Times.Once); + } + + [Test] + public async Task CheckPassword_PlexUser_UserNameLogin_ValidPassword() + { + var user = new OmbiUser + { + UserType = UserType.PlexUser, + EmailLogin = false, + Email = "MyEmail@email.com", + UserName = "heyhey" + }; + PlexApi.Setup(x => x.SignIn(It.IsAny())) + .ReturnsAsync(new PlexAuthentication + { + user = new User + { + authentication_token = "abc" + } + }); + var result = await _um.CheckPasswordAsync(user, "pass"); + + Assert.That(result, Is.True); + PlexApi.Verify(x => x.SignIn(It.Is(c => c.login == "heyhey")), Times.Once); + } + + [Test] + public async Task CheckPassword_PlexUser_UserNameLogin_InvalidPassword() + { + var user = new OmbiUser + { + UserType = UserType.PlexUser, + EmailLogin = false, + Email = "MyEmail@email.com", + UserName = "heyhey" + }; + PlexApi.Setup(x => x.SignIn(It.IsAny())) + .ReturnsAsync(new PlexAuthentication()); + var result = await _um.CheckPasswordAsync(user, "pass"); + + Assert.That(result, Is.False); + PlexApi.Verify(x => x.SignIn(It.Is(c => c.login == "heyhey")), Times.Once); + } + } +} diff --git a/src/Ombi.Core.Tests/Engine/CalendarEngineTests.cs b/src/Ombi.Core.Tests/Engine/CalendarEngineTests.cs new file mode 100644 index 000000000..b922a21c0 --- /dev/null +++ b/src/Ombi.Core.Tests/Engine/CalendarEngineTests.cs @@ -0,0 +1,195 @@ + +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using Moq; +using NUnit.Framework; +using Ombi.Core.Authentication; +using Ombi.Core.Engine.V2; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Core.Tests.Engine +{ + [TestFixture] + public class CalendarEngineTests + { + public Mock MovieRepo { get; set; } + public Mock TvRepo { get; set; } + public CalendarEngine CalendarEngine { get; set; } + + [SetUp] + public void Setup() + { + MovieRepo = new Mock(); + TvRepo = new Mock(); + var principle = new Mock(); + var identity = new Mock(); + identity.Setup(x => x.Name).Returns("UnitTest"); + principle.Setup(x => x.Identity).Returns(identity.Object); + CalendarEngine = new CalendarEngine(principle.Object, null, null, MovieRepo.Object, TvRepo.Object); + } + + [Test] + public async Task Calendar_Movies_OnlyGet_PreviousAndFuture_90_Days() + { + var movies = new List + { + new MovieRequests + { + Title="Invalid", + ReleaseDate = new DateTime(2018,10,01) + }, + new MovieRequests + { + Title="Invalid", + ReleaseDate = DateTime.Now.AddDays(91) + }, + + new MovieRequests + { + Title="Valid", + ReleaseDate = DateTime.Now + } + }; + MovieRepo.Setup(x => x.GetAll()).Returns(movies.AsQueryable()); + var data = await CalendarEngine.GetCalendarData(); + + Assert.That(data.Count, Is.EqualTo(1)); + Assert.That(data[0].Title, Is.EqualTo("Valid")); + } + + [Test] + public async Task Calendar_Episodes_OnlyGet_PreviousAndFuture_90_Days() + { + var tv = new List + { + new ChildRequests + { + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Title = "Invalid", + AirDate = new DateTime(2018,01,01) + }, + new EpisodeRequests + { + Title = "Invalid", + AirDate = DateTime.Now.AddDays(91) + }, + new EpisodeRequests + { + Title = "Valid", + AirDate = DateTime.Now + }, + } + } + } + }, + }; + TvRepo.Setup(x => x.GetChild()).Returns(tv.AsQueryable()); + var data = await CalendarEngine.GetCalendarData(); + + Assert.That(data.Count, Is.EqualTo(1)); + Assert.That(data[0].Title, Is.EqualTo("Valid")); + } + + + [TestCaseSource(nameof(StatusTvColorData))] + public async Task Calendar_Tv_StatusColor(AvailabilityTestModel model) + { + var tv = new List + { + new ChildRequests + { + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Title = "Valid", + AirDate = DateTime.Now, + Approved = model.Approved, + Available = model.Available + }, + } + } + } + }, + }; + TvRepo.Setup(x => x.GetChild()).Returns(tv.AsQueryable()); + var data = await CalendarEngine.GetCalendarData(); + + return data[0].BackgroundColor; + } + + [TestCaseSource(nameof(StatusColorData))] + public async Task Calendar_Movie_StatusColor(AvailabilityTestModel model) + { + var movies = new List + { + new MovieRequests + { + Title="Valid", + ReleaseDate = DateTime.Now, + Denied = model.Denied, + Approved = model.Approved, + Available = model.Available + }, + }; + MovieRepo.Setup(x => x.GetAll()).Returns(movies.AsQueryable()); + var data = await CalendarEngine.GetCalendarData(); + + return data[0].BackgroundColor; + } + + public static IEnumerable StatusColorData + { + get + { + yield return new TestCaseData(new AvailabilityTestModel + { + Approved = true, + Denied = true + }).Returns("red").SetName("Calendar_DeniedRequest"); + foreach (var testCaseData in StatusTvColorData) + { + yield return testCaseData; + } + } + } + + public static IEnumerable StatusTvColorData + { + get + { + yield return new TestCaseData(new AvailabilityTestModel + { + Available = true, + Approved = true + }).Returns("#469c83").SetName("Calendar_AvailableRequest"); + yield return new TestCaseData(new AvailabilityTestModel + { + Approved = true + }).Returns("blue").SetName("Calendar_ApprovedRequest"); + } + } + } + + public class AvailabilityTestModel + { + public bool Available { get; set; } + public bool Denied { get; set; } + public bool Approved { get; set; } + } +} diff --git a/src/Ombi.Core.Tests/Engine/VoteEngineTests.cs b/src/Ombi.Core.Tests/Engine/VoteEngineTests.cs index ad4c33131..41c645815 100644 --- a/src/Ombi.Core.Tests/Engine/VoteEngineTests.cs +++ b/src/Ombi.Core.Tests/Engine/VoteEngineTests.cs @@ -3,16 +3,19 @@ using System.Linq; using System.Security.Principal; using System.Threading.Tasks; using AutoFixture; +using MockQueryable.Moq; using Moq; using NUnit.Framework; using Ombi.Core.Authentication; using Ombi.Core.Engine; using Ombi.Core.Engine.Interfaces; +using Ombi.Core.Models; using Ombi.Core.Rule.Interfaces; using Ombi.Core.Settings; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; using Ombi.Store.Repository; +using Ombi.Test.Common; namespace Ombi.Core.Tests.Engine { @@ -30,12 +33,17 @@ namespace Ombi.Core.Tests.Engine MovieRequestEngine = new Mock(); MovieRequestEngine = new Mock(); User = new Mock(); - UserManager = new Mock(); - UserManager.Setup(x => x.Users) - .Returns(new EnumerableQuery(new List {new OmbiUser {Id = "abc"}})); + User.Setup(x => x.Identity.Name).Returns("abc"); + UserManager = MockHelper.MockUserManager(new List { new OmbiUser { Id = "abc", UserName = "abc" } }); Rule = new Mock(); Engine = new VoteEngine(VoteRepository.Object, User.Object, UserManager.Object, Rule.Object, VoteSettings.Object, MusicRequestEngine.Object, TvRequestEngine.Object, MovieRequestEngine.Object); + + F.Behaviors.OfType().ToList() + .ForEach(b => F.Behaviors.Remove(b)); + F.Behaviors.Add(new OmitOnRecursionBehavior()); + + } public Fixture F { get; set; } @@ -49,25 +57,160 @@ namespace Ombi.Core.Tests.Engine public Mock TvRequestEngine { get; set; } public Mock MovieRequestEngine { get; set; } - [Test] - [Ignore("Need to mock the user manager")] - public async Task New_Upvote() + [TestCaseSource(nameof(VoteData))] + public async Task Vote(VoteType type, RequestType request) { - VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings()); + VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings + { + Enabled = true, + MovieVoteMax = 10 + }); + var votes = F.CreateMany().ToList(); + + VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery(votes) + .AsQueryable() + .BuildMock().Object); + var result = new VoteEngineResult(); + if (type == VoteType.Downvote) + { + result = await Engine.DownVote(1, request); + } + else + { + result = await Engine.UpVote(1, request); + } + + Assert.That(result.Result, Is.True); + VoteRepository.Verify(x => x.Add(It.Is(c => c.UserId == "abc" && c.VoteType == type)), Times.Once); + VoteRepository.Verify(x => x.Delete(It.IsAny()), Times.Never); + MovieRequestEngine.Verify(x => x.ApproveMovieById(1), Times.Never); + } + public static IEnumerable VoteData + { + + get + { + yield return new TestCaseData(VoteType.Upvote, RequestType.Movie).SetName("Movie_Upvote"); + yield return new TestCaseData(VoteType.Downvote, RequestType.Movie).SetName("Movie_Downvote"); + yield return new TestCaseData(VoteType.Upvote, RequestType.TvShow).SetName("Tv_Upvote"); + yield return new TestCaseData(VoteType.Downvote, RequestType.TvShow).SetName("Tv_Downvote"); + } + } + + + [TestCaseSource(nameof(AttemptedTwiceData))] + public async Task Attempted_Twice(VoteType type, RequestType request) + { + VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings + { + Enabled = true, + MovieVoteMax = 10 + }); var votes = F.CreateMany().ToList(); votes.Add(new Votes { RequestId = 1, RequestType = RequestType.Movie, - UserId = "abc" + UserId = "abc", + VoteType = type }); - VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery(votes)); - var result = await Engine.UpVote(1, RequestType.Movie); + VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery(votes) + .AsQueryable() + .BuildMock().Object); + var result = new VoteEngineResult(); + if (type == VoteType.Downvote) + { + result = await Engine.DownVote(1, request); + } + else + { + result = await Engine.UpVote(1, request); + } + + Assert.That(result.Result, Is.False); + VoteRepository.Verify(x => x.Delete(It.IsAny()), Times.Never); + MovieRequestEngine.Verify(x => x.ApproveMovieById(1), Times.Never); + } + public static IEnumerable AttemptedTwiceData + { + + get + { + yield return new TestCaseData(VoteType.Upvote, RequestType.Movie).SetName("Upvote_Attemped_Twice_Movie"); + yield return new TestCaseData(VoteType.Downvote, RequestType.Movie).SetName("Downvote_Attempted_Twice_Movie"); + yield return new TestCaseData(VoteType.Upvote, RequestType.TvShow).SetName("Upvote_Attemped_Twice_Tv"); + yield return new TestCaseData(VoteType.Downvote, RequestType.TvShow).SetName("Downvote_Attempted_Twice_Tv"); + } + } + + [TestCaseSource(nameof(VoteConvertData))] + public async Task Downvote_Converted_To_Upvote(VoteType type, RequestType request) + { + VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings + { + Enabled = true, + MovieVoteMax = 10 + }); + var votes = F.CreateMany().ToList(); + votes.Add(new Votes + { + RequestId = 1, + RequestType = request, + UserId = "abc", + VoteType = type == VoteType.Upvote ? VoteType.Downvote : VoteType.Upvote + }); + VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery(votes) + .AsQueryable() + .BuildMock().Object); + + var result = new VoteEngineResult(); + if (type == VoteType.Downvote) + { + result = await Engine.DownVote(1, request); + } + else + { + result = await Engine.UpVote(1, request); + } + Assert.That(result.Result, Is.True); + VoteRepository.Verify(x => x.Delete(It.IsAny()), Times.Once); + VoteRepository.Verify(x => x.Add(It.Is(v => v.VoteType == type)), Times.Once); + MovieRequestEngine.Verify(x => x.ApproveMovieById(1), Times.Never); + } + public static IEnumerable VoteConvertData + { + + get + { + yield return new TestCaseData(VoteType.Upvote, RequestType.Movie).SetName("Downvote_Converted_To_UpVote_Movie"); + yield return new TestCaseData(VoteType.Downvote, RequestType.Movie).SetName("UpVote_Converted_To_DownVote_Movie"); + yield return new TestCaseData(VoteType.Upvote, RequestType.TvShow).SetName("Downvote_Converted_To_UpVote_TvShow"); + yield return new TestCaseData(VoteType.Downvote, RequestType.TvShow).SetName("UpVote_Converted_To_DownVote_TvShow"); + } + } + + + [TestCaseSource(nameof(VotingDisabledData))] + public async Task Voting_Disabled(RequestType type) + { + VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings + { + Enabled = false, + MovieVoteMax = 10 + }); + + var result = await Engine.UpVote(1, type); Assert.That(result.Result, Is.True); - VoteRepository.Verify(x => x.Add(It.Is(c => c.UserId == "abc" && c.VoteType == VoteType.Upvote)), Times.Once); - VoteRepository.Verify(x => x.Delete(It.IsAny()), Times.Once); - MovieRequestEngine.Verify(x => x.ApproveMovieById(1), Times.Never); + VoteRepository.Verify(x => x.Add(It.IsAny()), Times.Never); + } + public static IEnumerable VotingDisabledData + { + get + { + yield return new TestCaseData(RequestType.Movie).SetName("Voting_Disabled_Movie"); + yield return new TestCaseData(RequestType.TvShow).SetName("Voting_Disabled_TV"); + } } } } \ No newline at end of file diff --git a/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj b/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj index 23fc6db78..ed43d4991 100644 --- a/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj +++ b/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj @@ -7,14 +7,16 @@ - + - - + + + + diff --git a/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs index 7ff8283da..b4d41cb4b 100644 --- a/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs @@ -4,29 +4,43 @@ using Moq; using Ombi.Core.Rule.Rules.Request; using Ombi.Store.Entities.Requests; using NUnit.Framework; +using Ombi.Core.Authentication; using Ombi.Helpers; +using Ombi.Test.Common; +using System.Collections.Generic; +using Ombi.Store.Entities; +using System; namespace Ombi.Core.Tests.Rule.Request { [TestFixture] public class AutoApproveRuleTests { + private List _users = new List + { + new OmbiUser { Id = Guid.NewGuid().ToString("N"), UserName="abc" } + }; + [SetUp] public void Setup() { PrincipalMock = new Mock(); - Rule = new AutoApproveRule(PrincipalMock.Object); + PrincipalMock.Setup(x => x.Identity.Name).Returns("abc"); + + UserManager = MockHelper.MockUserManager(_users); + Rule = new AutoApproveRule(PrincipalMock.Object, UserManager.Object); } private AutoApproveRule Rule { get; set; } private Mock PrincipalMock { get; set; } + private Mock UserManager { get; set; } [Test] public async Task Should_ReturnSuccess_WhenAdminAndRequestMovie() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.Admin)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -37,7 +51,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnSuccess_WhenAdminAndRequestTV() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.Admin)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -48,7 +62,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnSuccess_WhenAutoApproveMovieAndRequestMovie() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.AutoApproveMovie)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.AutoApproveMovie)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -56,10 +70,21 @@ namespace Ombi.Core.Tests.Rule.Request Assert.True(request.Approved); } + [Test] + public async Task Should_ReturnFail_WhenAutoApproveMovie_And_RequestTV() + { + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.AutoApproveMovie)).ReturnsAsync(true); + var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + } + [Test] public async Task Should_ReturnSuccess_WhenAutoApproveTVAndRequestTV() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.AutoApproveTv)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.AutoApproveTv)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -67,9 +92,21 @@ namespace Ombi.Core.Tests.Rule.Request Assert.True(request.Approved); } + [Test] + public async Task Should_ReturnFail_WhenAutoApproveTV_And_RequestMovie() + { + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.AutoApproveTv)).ReturnsAsync(true); + var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + } + [Test] public async Task Should_ReturnFail_WhenNoClaimsAndRequestMovie() { + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -80,6 +117,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnFail_WhenNoClaimsAndRequestTV() { + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); diff --git a/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs index c9db5875a..a1630af92 100644 --- a/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Request/CanRequestRuleTests.cs @@ -1,31 +1,46 @@ +using System; +using System.Collections.Generic; using System.Security.Principal; using System.Threading.Tasks; using Moq; using NUnit.Framework; +using Ombi.Core.Authentication; using Ombi.Core.Rule.Rules; +using Ombi.Core.Rule.Rules.Request; using Ombi.Helpers; +using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; +using Ombi.Test.Common; namespace Ombi.Core.Tests.Rule.Request { public class CanRequestRuleTests { + private List _users = new List + { + new OmbiUser { Id = Guid.NewGuid().ToString("N"), UserName="abc" } + }; + [SetUp] public void Setup() { PrincipalMock = new Mock(); - Rule = new CanRequestRule(PrincipalMock.Object); + PrincipalMock.Setup(x => x.Identity.Name).Returns("abc"); + + UserManager = MockHelper.MockUserManager(_users); + Rule = new CanRequestRule(PrincipalMock.Object, UserManager.Object); } private CanRequestRule Rule { get; set; } private Mock PrincipalMock { get; set; } + private Mock UserManager { get; set; } [Test] public async Task Should_ReturnSuccess_WhenRequestingMovieWithMovieRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestMovie)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.RequestMovie)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -35,7 +50,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnFail_WhenRequestingMovieWithoutMovieRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestMovie)).Returns(false); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.RequestMovie)).ReturnsAsync(false); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -46,7 +61,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnSuccess_WhenRequestingMovieWithAdminRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.Admin)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); @@ -56,7 +71,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnSuccess_WhenRequestingTVWithAdminRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.Admin)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.Admin)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -66,7 +81,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnSuccess_WhenRequestingTVWithTVRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestTv)).Returns(true); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.RequestTv)).ReturnsAsync(true); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); @@ -76,7 +91,7 @@ namespace Ombi.Core.Tests.Rule.Request [Test] public async Task Should_ReturnFail_WhenRequestingTVWithoutTVRole() { - PrincipalMock.Setup(x => x.IsInRole(OmbiRoles.RequestTv)).Returns(false); + UserManager.Setup(x => x.IsInRoleAsync(It.IsAny(), OmbiRoles.RequestTv)).ReturnsAsync(false); var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); diff --git a/src/Ombi.Core.Tests/Rule/Request/ExistingMovieRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/ExistingMovieRequestRuleTests.cs new file mode 100644 index 000000000..7ff69c9f2 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Request/ExistingMovieRequestRuleTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using MockQueryable.Moq; +using Moq; +using NUnit.Framework; +using Ombi.Core.Authentication; +using Ombi.Core.Rule.Rules; +using Ombi.Core.Rule.Rules.Request; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; +using Ombi.Test.Common; + +namespace Ombi.Core.Tests.Rule.Request +{ + public class ExistingMovieRequestRuleTests + { + + [SetUp] + public void Setup() + { + ContextMock = new Mock(); + Rule = new ExistingMovieRequestRule(ContextMock.Object); + } + + + private ExistingMovieRequestRule Rule { get; set; } + private Mock ContextMock { get; set; } + + [Test] + public async Task ExistingRequestRule_Movie_Has_Been_Requested_With_TheMovieDBId() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new MovieRequests + { + TheMovieDbId = 1, + RequestType = RequestType.Movie + } + }.AsQueryable().BuildMock().Object); + var o = new MovieRequests + { + TheMovieDbId = 1, + }; + var result = await Rule.Execute(o); + + Assert.That(result.Success, Is.False); + Assert.That(result.Message, Is.Not.Empty); + } + + [Test] + public async Task ExistingRequestRule_Movie_Has_Been_Requested_With_ImdbId() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new MovieRequests + { + TheMovieDbId = 11111, + ImdbId = 1.ToString(), + RequestType = RequestType.Movie + } + }.AsQueryable().BuildMock().Object); + var o = new MovieRequests + { + ImdbId = 1.ToString(), + }; + var result = await Rule.Execute(o); + + Assert.That(result.Success, Is.False); + Assert.That(result.Message, Is.Not.Empty); + } + + [Test] + public async Task ExistingRequestRule_Movie_HasNot_Been_Requested() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new MovieRequests + { + TheMovieDbId = 2, + ImdbId = "2", + RequestType = RequestType.Movie + } + }.AsQueryable().BuildMock().Object); + var o = new MovieRequests + { + TheMovieDbId = 1, + ImdbId = "1" + }; + var result = await Rule.Execute(o); + + Assert.That(result.Success, Is.True); + Assert.That(result.Message, Is.Null.Or.Empty); + } + } +} diff --git a/src/Ombi.Core.Tests/Rule/Search/AvailabilityRuleHelperTests.cs b/src/Ombi.Core.Tests/Rule/Search/AvailabilityRuleHelperTests.cs new file mode 100644 index 000000000..7580ed9c8 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Search/AvailabilityRuleHelperTests.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Ombi.Core.Models.Search; +using Ombi.Core.Rule.Rules.Search; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Core.Tests.Rule.Search +{ + public class AvailabilityRuleHelperTests + { + + + [Test] + public void Is_Available_When_All_We_Have_All_Aired_Episodes() + { + var episodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-1), // Yesterday + Available = true + }, + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(1), // Tomorrow! + Available = false + } + }; + + var model = new SearchTvShowViewModel + { + SeasonRequests = new List { new SeasonRequests { Episodes = episodes } } + }; + AvailabilityRuleHelper.CheckForUnairedEpisodes(model); + Assert.That(model.FullyAvailable, Is.True); + } + + [Test] + public void Is_Available_When_All_We_Have_All_Aired_Episodes_With_Unknown_Dates() + { + var episodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-1), // Yesterday + Available = true + }, + new EpisodeRequests + { + AirDate = DateTime.MinValue, // Unknown date! + Available = false + } + }; + + var model = new SearchTvShowViewModel + { + SeasonRequests = new List { new SeasonRequests { Episodes = episodes } } + }; + AvailabilityRuleHelper.CheckForUnairedEpisodes(model); + Assert.That(model.FullyAvailable, Is.True); + } + + [Test] + public void Is_PartlyAvailable_When_All_We_Have_Some_Aired_Episodes() + { + var episodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-1), // Yesterday + Available = true + }, + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-14), // Yesterday + Available = false + }, + new EpisodeRequests + { + AirDate = DateTime.MinValue, // Unknown date! + Available = false + } + }; + + var model = new SearchTvShowViewModel + { + SeasonRequests = new List { new SeasonRequests { Episodes = episodes } } + }; + AvailabilityRuleHelper.CheckForUnairedEpisodes(model); + Assert.That(model.FullyAvailable, Is.False); + Assert.That(model.PartlyAvailable, Is.True); + } + + [Test] + public void Is_SeasonAvailable_When_All_We_Have_All_Aired_Episodes_In_A_Season() + { + var episodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-1), // Yesterday + Available = true + }, + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-14), // Yesterday + Available = false + }, + new EpisodeRequests + { + AirDate = DateTime.MinValue, // Unknown date! + Available = false + } + }; + + var availableEpisodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-1), // Yesterday + Available = true + }, + }; + + var model = new SearchTvShowViewModel + { + SeasonRequests = new List + { + new SeasonRequests { Episodes = episodes }, + new SeasonRequests { Episodes = availableEpisodes }, + } + }; + AvailabilityRuleHelper.CheckForUnairedEpisodes(model); + Assert.That(model.FullyAvailable, Is.False); + Assert.That(model.PartlyAvailable, Is.True); + Assert.That(model.SeasonRequests[1].SeasonAvailable, Is.True); + } + + [Test] + public void Is_NotAvailable_When_All_We_Have_No_Aired_Episodes() + { + var episodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-1), // Yesterday + Available = false + }, + new EpisodeRequests + { + AirDate = DateTime.Now.AddDays(-14), + Available = false + }, + new EpisodeRequests + { + AirDate = DateTime.MinValue, // Unknown date! + Available = false + } + }; + + var model = new SearchTvShowViewModel + { + SeasonRequests = new List { new SeasonRequests { Episodes = episodes } } + }; + AvailabilityRuleHelper.CheckForUnairedEpisodes(model); + Assert.That(model.FullyAvailable, Is.False); + Assert.That(model.PartlyAvailable, Is.False); + } + [Test] + public void Is_NotAvailable_When_All_Episodes_Are_Unknown() + { + var episodes = new List + { + new EpisodeRequests + { + AirDate = DateTime.MinValue, + Available = false + }, + new EpisodeRequests + { + AirDate = DateTime.MinValue, + Available = false + }, + new EpisodeRequests + { + AirDate = DateTime.MinValue, // Unknown date! + Available = false + } + }; + + var model = new SearchTvShowViewModel + { + SeasonRequests = new List { new SeasonRequests { Episodes = episodes } } + }; + AvailabilityRuleHelper.CheckForUnairedEpisodes(model); + Assert.That(model.FullyAvailable, Is.False); + Assert.That(model.PartlyAvailable, Is.False); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs index 99ff5b6bd..8aac99464 100644 --- a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs @@ -4,6 +4,8 @@ using Moq; using NUnit.Framework; using Ombi.Core.Models.Search; using Ombi.Core.Rule.Rules.Search; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; using Ombi.Store.Entities; using Ombi.Store.Repository; using Ombi.Store.Repository.Requests; @@ -16,15 +18,18 @@ namespace Ombi.Core.Tests.Rule.Search public void Setup() { ContextMock = new Mock(); - Rule = new EmbyAvailabilityRule(ContextMock.Object); + SettingsMock = new Mock>(); + Rule = new EmbyAvailabilityRule(ContextMock.Object, SettingsMock.Object); } private EmbyAvailabilityRule Rule { get; set; } private Mock ContextMock { get; set; } + private Mock> SettingsMock { get; set; } [Test] public async Task Movie_ShouldBe_Available_WhenFoundInEmby() { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new EmbySettings()); ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new EmbyContent { ProviderId = "123" @@ -39,6 +44,64 @@ namespace Ombi.Core.Tests.Rule.Search Assert.True(search.Available); } + [Test] + public async Task Movie_Has_Custom_Url_When_Specified_In_Settings() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new EmbySettings + { + Enable = true, + Servers = new List + { + new EmbyServers + { + ServerHostname = "http://test.com/" + } + } + }); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new EmbyContent + { + ProviderId = "123", + EmbyId = 1.ToString() + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.That(search.EmbyUrl, Is.EqualTo("http://test.com/#!/itemdetails.html?id=1")); + } + + [Test] + public async Task Movie_Uses_Default_Url_When() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new EmbySettings + { + Enable = true, + Servers = new List + { + new EmbyServers + { + ServerHostname = string.Empty + } + } + }); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new EmbyContent + { + ProviderId = "123", + EmbyId = 1.ToString() + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.That(search.EmbyUrl, Is.EqualTo("https://app.emby.media/#!/itemdetails.html?id=1")); + } + [Test] public async Task Movie_ShouldBe_NotAvailable_WhenNotFoundInEmby() { diff --git a/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs index e32c8e996..ea9208cf7 100644 --- a/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs @@ -11,7 +11,7 @@ using Ombi.Store.Repository.Requests; namespace Ombi.Core.Tests.Rule.Search { - public class ExistignRequestRuleTests + public class ExistingRequestRuleTests { [SetUp] public void Setup() @@ -39,18 +39,16 @@ namespace Ombi.Core.Tests.Rule.Search RequestType = RequestType.Movie }; - MovieMock.Setup(x => x.GetRequest(123)).Returns(list); + MovieMock.Setup(x => x.GetRequestAsync(123)).ReturnsAsync(list); var search = new SearchMovieViewModel { Id = 123, - - }; var result = await Rule.Execute(search); - Assert.True(result.Success); - Assert.True(search.Approved); - Assert.True(search.Requested); + Assert.That(result.Success, Is.True); + Assert.That(search.Approved, Is.True); + Assert.That(search.Requested, Is.True); } [Test] @@ -62,7 +60,7 @@ namespace Ombi.Core.Tests.Rule.Search Approved = true }; - MovieMock.Setup(x => x.GetRequest(123)).Returns(list); + MovieMock.Setup(x => x.GetRequestAsync(123)).ReturnsAsync(list); var search = new SearchMovieViewModel { Id = 999, diff --git a/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs new file mode 100644 index 000000000..6b04a57b7 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs @@ -0,0 +1,126 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using Ombi.Core.Models.Search; +using Ombi.Core.Rule.Rules.Search; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Core.Tests.Rule.Search +{ + public class LidarrAlbumCacheRuleTests + { + [SetUp] + public void Setup() + { + ContextMock = new Mock>(); + Rule = new LidarrAlbumCacheRule(ContextMock.Object); + + } + + private LidarrAlbumCacheRule Rule { get; set; } + private Mock> ContextMock { get; set; } + + [Test] + public async Task Should_Not_Be_Monitored_Or_Available() + { + var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + Assert.False(request.Monitored); + } + + [Test] + public async Task Should_Be_Monitored_But_Not_Available() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new LidarrAlbumCache + { + ForeignAlbumId = "abc", + PercentOfTracks = 0 + } + }.AsQueryable()); + var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + Assert.True(request.Monitored); + Assert.That(request.PartiallyAvailable, Is.EqualTo(false)); + Assert.That(request.Available, Is.EqualTo(false)); + Assert.That(request.FullyAvailable, Is.EqualTo(false)); + } + + [Test] + public async Task Should_Be_Monitored_And_Partly_Available() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new LidarrAlbumCache + { + ForeignAlbumId = "abc", + PercentOfTracks = 1 + } + }.AsQueryable()); + var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + Assert.True(request.Monitored); + Assert.That(request.PartiallyAvailable, Is.EqualTo(true)); + Assert.That(request.Available, Is.EqualTo(false)); + Assert.That(request.FullyAvailable, Is.EqualTo(false)); + } + + [Test] + public async Task Should_Be_Monitored_And_Fully_Available() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new LidarrAlbumCache + { + ForeignAlbumId = "abc", + PercentOfTracks = 100 + } + }.AsQueryable()); + var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + Assert.True(request.Monitored); + Assert.That(request.PartiallyAvailable, Is.EqualTo(false)); + Assert.That(request.FullyAvailable, Is.EqualTo(true)); + } + + [Test] + public async Task Should_Be_Monitored_And_Fully_Available_Casing() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new LidarrAlbumCache + { + ForeignAlbumId = "abc", + PercentOfTracks = 100 + } + }.AsQueryable()); + var request = new SearchAlbumViewModel { ForeignAlbumId = "ABC" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Approved); + Assert.True(request.Monitored); + Assert.That(request.PartiallyAvailable, Is.EqualTo(false)); + Assert.That(request.FullyAvailable, Is.EqualTo(true)); + } + } +} diff --git a/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs new file mode 100644 index 000000000..7a2da1e25 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs @@ -0,0 +1,74 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using Ombi.Core.Models.Search; +using Ombi.Core.Rule.Rules.Search; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Core.Tests.Rule.Search +{ + public class LidarrArtistCacheRuleTests + { + [SetUp] + public void Setup() + { + ContextMock = new Mock>(); + Rule = new LidarrArtistCacheRule(ContextMock.Object); + } + + private LidarrArtistCacheRule Rule { get; set; } + private Mock> ContextMock { get; set; } + + [Test] + public async Task Should_Not_Be_Monitored() + { + var request = new SearchArtistViewModel { ForignArtistId = "abc" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.False(request.Monitored); + } + + [Test] + public async Task Should_Be_Monitored() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new LidarrArtistCache + { + ForeignArtistId = "abc", + } + }.AsQueryable()); + var request = new SearchArtistViewModel { ForignArtistId = "abc" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.True(request.Monitored); + } + + + [Test] + public async Task Should_Be_Monitored_Casing() + { + ContextMock.Setup(x => x.GetAll()).Returns(new List + { + new LidarrArtistCache + { + ForeignArtistId = "abc", + } + }.AsQueryable()); + var request = new SearchArtistViewModel { ForignArtistId = "ABC" }; + var result = await Rule.Execute(request); + + Assert.True(result.Success); + Assert.True(request.Monitored); + } + + } +} diff --git a/src/Ombi.Core.Tests/Rule/Search/PlexAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/PlexAvailabilityRuleTests.cs deleted file mode 100644 index 55177a6ac..000000000 --- a/src/Ombi.Core.Tests/Rule/Search/PlexAvailabilityRuleTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Threading.Tasks; -using Moq; -using NUnit.Framework; -using Ombi.Core.Models.Search; -using Ombi.Core.Rule.Rules.Search; -using Ombi.Store.Entities; -using Ombi.Store.Repository; - -namespace Ombi.Core.Tests.Rule.Search -{ - public class PlexAvailabilityRuleTests - { - [SetUp] - public void Setup() - { - ContextMock = new Mock(); - Rule = new PlexAvailabilityRule(ContextMock.Object); - } - - private PlexAvailabilityRule Rule { get; set; } - private Mock ContextMock { get; set; } - - [Test] - public async Task ShouldBe_Available_WhenFoundInPlex() - { - ContextMock.Setup(x => x.Get(It.IsAny())).ReturnsAsync(new PlexServerContent - { - Url = "TestUrl", - ImdbId = "132" - }); - - var search = new SearchMovieViewModel - { - ImdbId = "123", - }; - var result = await Rule.Execute(search); - - Assert.True(result.Success); - Assert.AreEqual("TestUrl", search.PlexUrl); - Assert.True(search.Available); - } - - [Test] - public async Task ShouldBe_NotAvailable_WhenNotFoundInPlex() - { - ContextMock.Setup(x => x.Get(It.IsAny())).Returns(Task.FromResult(default(PlexServerContent))); - var search = new SearchMovieViewModel(); - var result = await Rule.Execute(search); - - Assert.True(result.Success); - Assert.Null(search.PlexUrl); - Assert.False(search.Available); - } - } -} \ No newline at end of file diff --git a/src/Ombi.Core.Tests/StringHelperTests.cs b/src/Ombi.Core.Tests/StringHelperTests.cs index c1b95fcd7..dcd05ae4c 100644 --- a/src/Ombi.Core.Tests/StringHelperTests.cs +++ b/src/Ombi.Core.Tests/StringHelperTests.cs @@ -18,7 +18,7 @@ namespace Ombi.Core.Tests { get { - yield return new TestCaseData("this!is^a*string",new []{'!','^','*'}).Returns("thisisastring").SetName("Basic Strip Multipe Chars"); + yield return new TestCaseData("this!is^a*string",new []{'!','^','*'}).Returns("thisisastring").SetName("Basic Strip Multiple Chars"); yield return new TestCaseData("What is this madness'",new []{'\'','^','*'}).Returns("What is this madness").SetName("Basic Strip Multipe Chars"); } } diff --git a/src/Ombi.Core/Engine/Demo/DemoMovieSearchEngine.cs b/src/Ombi.Core/Engine/Demo/DemoMovieSearchEngine.cs new file mode 100644 index 000000000..86582fb4d --- /dev/null +++ b/src/Ombi.Core/Engine/Demo/DemoMovieSearchEngine.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Config; +using Ombi.Core.Authentication; +using Ombi.Core.Models.Requests; +using Ombi.Core.Models.Search; +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; + +namespace Ombi.Core.Engine.Demo +{ + public class DemoMovieSearchEngine : MovieSearchEngine, IDemoMovieSearchEngine + { + public DemoMovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, + ILogger logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService s, + IRepository sub, IOptions lists) + : base(identity, service, movApi, mapper, logger, r, um, mem, s, sub) + { + _demoLists = lists.Value; + } + + private readonly DemoLists _demoLists; + + public async Task> Search(string search) + { + var result = await MovieApi.SearchMovie(search, null, "en"); + + for (var i = 0; i < result.Count; i++) + { + if (!_demoLists.Movies.Contains(result[i].Id)) + { + result.RemoveAt(i); + } + } + if(result.Count > 0) + return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API + return null; + } + + public async Task> NowPlayingMovies() + { + var rand = new Random(); + var responses = new List(); + for (int i = 0; i < 10; i++) + { + var item = rand.Next(_demoLists.Movies.Length); + var movie = _demoLists.Movies[item]; + if (responses.Any(x => x.Id == movie)) + { + i--; + continue; + } + var movieResult = await MovieApi.GetMovieInformationWithExtraInfo(movie); + var viewMovie = Mapper.Map(movieResult); + + responses.Add(await ProcessSingleMovie(viewMovie)); + } + + return responses; + } + + public async Task> PopularMovies() + { + return await NowPlayingMovies(); + } + + + public async Task> TopRatedMovies() + { + return await NowPlayingMovies(); + } + + public async Task> UpcomingMovies() + { + + return await NowPlayingMovies(); + } + } + + public interface IDemoMovieSearchEngine + { + Task> NowPlayingMovies(); + + Task> PopularMovies(); + + Task> Search(string search); + + Task> TopRatedMovies(); + + Task> UpcomingMovies(); + + } +} diff --git a/src/Ombi.Core/Engine/Demo/DemoTvSearchEngine.cs b/src/Ombi.Core/Engine/Demo/DemoTvSearchEngine.cs new file mode 100644 index 000000000..edf9c430d --- /dev/null +++ b/src/Ombi.Core/Engine/Demo/DemoTvSearchEngine.cs @@ -0,0 +1,96 @@ +using AutoMapper; +using Microsoft.Extensions.Options; +using Ombi.Api.Trakt; +using Ombi.Api.TvMaze; +using Ombi.Config; +using Ombi.Core.Authentication; +using Ombi.Core.Models.Requests; +using Ombi.Core.Models.Search; +using Ombi.Core.Rule.Interfaces; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Settings.Settings.Models; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; + +namespace Ombi.Core.Engine.Demo +{ + public class DemoTvSearchEngine : TvSearchEngine, IDemoTvSearchEngine + { + + public DemoTvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, + ISettingsService plexSettings, ISettingsService embySettings, IPlexContentRepository repo, + IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, + ISettingsService s, IRepository sub, IOptions lists) + : base(identity, service, tvMaze, mapper, plexSettings, embySettings, repo, embyRepo, trakt, r, um, memCache, s, sub) + { + _demoLists = lists.Value; + } + + private readonly DemoLists _demoLists; + + public async Task> Search(string search) + { + var searchResult = await TvMazeApi.Search(search); + + for (var i = 0; i < searchResult.Count; i++) + { + if (!_demoLists.TvShows.Contains(searchResult[i].show?.externals?.thetvdb ?? 0)) + { + searchResult.RemoveAt(i); + } + } + + if (searchResult != null) + { + var retVal = new List(); + foreach (var tvMazeSearch in searchResult) + { + if (tvMazeSearch.show.externals == null || !(tvMazeSearch.show.externals?.thetvdb.HasValue ?? false)) + { + continue; + } + retVal.Add(ProcessResult(tvMazeSearch)); + } + return retVal; + } + return null; + } + + public async Task> NowPlayingMovies() + { + var rand = new Random(); + var responses = new List(); + for (int i = 0; i < 10; i++) + { + var item = rand.Next(_demoLists.TvShows.Length); + var tv = _demoLists.TvShows[item]; + if (responses.Any(x => x.Id == tv)) + { + i--; + continue; + } + + var movieResult = await TvMazeApi.ShowLookup(tv); + responses.Add(ProcessResult(movieResult)); + } + + return responses; + } + + + + } + + public interface IDemoTvSearchEngine + { + Task> Search(string search); + Task> NowPlayingMovies(); + } +} diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 8e34c4f19..20b5db148 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -1,12 +1,24 @@ -using Ombi.Core.Models.Search; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Ombi.Core.Models.Search; using Ombi.Core.Models.Search.V2; -namespace Ombi.Core +namespace Ombi.Core.Engine.Interfaces { public interface IMovieEngineV2 { Task GetFullMovieInformation(int theMovieDbId, string langCode = null); + Task> SimilarMovies(int theMovieDbId, string langCode); + Task> PopularMovies(); + Task> TopRatedMovies(); + Task> UpcomingMovies(); + Task> NowPlayingMovies(); + Task> NowPlayingMovies(int currentPosition, int amountToLoad); + Task GetCollection(int collectionId, string langCode = null); + Task GetTvDbId(int theMovieDbId); + Task> PopularMovies(int currentlyLoaded, int toLoad); + Task> TopRatedMovies(int currentlyLoaded, int toLoad); + Task> UpcomingMovies(int currentlyLoaded, int toLoad); + int ResultLimit { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs index d741dc8bc..bc1fa68d1 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Ombi.Core.Models.Requests; +using Ombi.Core.Models.UI; using Ombi.Store.Entities.Requests; namespace Ombi.Core.Engine.Interfaces @@ -13,10 +14,11 @@ namespace Ombi.Core.Engine.Interfaces Task RemoveMovieRequest(int requestId); Task RemoveAllMovieRequests(); - + Task GetRequest(int requestId); Task UpdateMovieRequest(MovieRequests request); Task ApproveMovie(MovieRequests request); Task ApproveMovieById(int requestId); Task DenyMovieById(int modelId, string denyReason); + Task> GetRequests(int count, int position, string sortProperty, string sortOrder); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/ITvRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/ITvRequestEngine.cs index 63de72bd1..ded696c0f 100644 --- a/src/Ombi.Core/Engine/Interfaces/ITvRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/ITvRequestEngine.cs @@ -23,5 +23,6 @@ namespace Ombi.Core.Engine.Interfaces Task> GetRequestsLite(); Task UpdateQualityProfile(int requestId, int profileId); Task UpdateRootPath(int requestId, int rootPath); + Task> GetRequests(int count, int position, string sortProperty, string sortOrder); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngine.cs b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngine.cs index 1ca119862..501197fb9 100644 --- a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngine.cs @@ -9,9 +9,13 @@ namespace Ombi.Core.Engine.Interfaces Task> Search(string searchTerm); Task GetShowInformation(int tvdbid); Task> Popular(); + Task> Popular(int currentlyLoaded, int amountToLoad); Task> Anticipated(); + Task> Anticipated(int currentlyLoaded, int amountToLoad); Task> MostWatches(); Task> Trending(); + Task> MostWatches(int currentlyLoaded, int amountToLoad); + Task> Trending(int currentlyLoaded, int amountToLoad); int ResultLimit { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs new file mode 100644 index 000000000..0a18a32bf --- /dev/null +++ b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ombi.Core.Models.Search.V2; + +namespace Ombi.Core +{ + public interface ITVSearchEngineV2 + { + Task GetShowInformation(int tvdbid); + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 456ba267a..231c1d062 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -4,6 +4,7 @@ using Ombi.Helpers; using Ombi.Store.Entities; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Security.Principal; @@ -200,6 +201,54 @@ namespace Ombi.Core.Engine }; } + public async Task> GetRequests(int count, int position, string sortProperty, string sortOrder) + { + var shouldHide = await HideFromOtherUsers(); + IQueryable allRequests; + if (shouldHide.Hide) + { + allRequests = + MovieRepository.GetWithUser(shouldHide + .UserId); + } + else + { + allRequests = + MovieRepository + .GetWithUser(); + } + + var prop = TypeDescriptor.GetProperties(typeof(MovieRequests)).Find(sortProperty, true); + + if (sortProperty.Contains('.')) + { + // This is a navigation property currently not supported + prop = TypeDescriptor.GetProperties(typeof(MovieRequests)).Find("RequestedDate", true); + //var properties = sortProperty.Split(new []{'.'}, StringSplitOptions.RemoveEmptyEntries); + //var firstProp = TypeDescriptor.GetProperties(typeof(MovieRequests)).Find(properties[0], true); + //var propType = firstProp.PropertyType; + //var secondProp = TypeDescriptor.GetProperties(propType).Find(properties[1], true); + } + + allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) + ? allRequests.OrderBy(x => prop.GetValue(x)) + : allRequests.OrderByDescending(x => prop.GetValue(x)); + var total = await allRequests.CountAsync(); + var requests = await allRequests.Skip(position).Take(count) + .ToListAsync(); + requests.ForEach(async x => + { + x.PosterPath = PosterPathHelper.FixPosterPath(x.PosterPath); + await CheckForSubscription(shouldHide, x); + }); + return new RequestsViewModel + { + Collection = requests, + Total = total + }; + + } + private IQueryable OrderMovies(IQueryable allRequests, OrderType type) { switch (type) @@ -259,6 +308,15 @@ namespace Ombi.Core.Engine return allRequests; } + public async Task GetRequest(int requestId) + { + var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync(); + request.PosterPath = PosterPathHelper.FixPosterPath(request.PosterPath); + await CheckForSubscription(new HideResult(), request); + + return request; + } + private async Task CheckForSubscription(HideResult shouldHide, MovieRequests x) { if (shouldHide.UserId == x.RequestedUserId) @@ -493,7 +551,7 @@ namespace Ombi.Core.Engine RequestType = RequestType.Movie, }); - return new RequestEngineResult {Result = true, Message = $"{movieName} has been successfully added!", RequestId = model.Id}; + return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!", RequestId = model.Id }; } public async Task GetRemainingRequests(OmbiUser user) @@ -533,7 +591,7 @@ namespace Ombi.Core.Engine return new RequestQuotaCountModel() { - HasLimit = true, + HasLimit = true, Limit = limit, Remaining = count, NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc), diff --git a/src/Ombi.Core/Engine/MovieSearchEngine.cs b/src/Ombi.Core/Engine/MovieSearchEngine.cs index 692776f72..4737c3992 100644 --- a/src/Ombi.Core/Engine/MovieSearchEngine.cs +++ b/src/Ombi.Core/Engine/MovieSearchEngine.cs @@ -31,10 +31,11 @@ namespace Ombi.Core.Engine Logger = logger; } - private IMovieDbApi MovieApi { get; } - private IMapper Mapper { get; } + protected IMovieDbApi MovieApi { get; } + protected IMapper Mapper { get; } private ILogger Logger { get; } + protected const int MovieLimit = 10; /// /// Lookups the imdb information. @@ -185,7 +186,7 @@ namespace Ombi.Core.Engine return null; } - private async Task> TransformMovieResultsToResponse( + protected async Task> TransformMovieResultsToResponse( IEnumerable movies) { var viewMovies = new List(); @@ -196,7 +197,7 @@ namespace Ombi.Core.Engine return viewMovies; } - private async Task ProcessSingleMovie(SearchMovieViewModel viewMovie, bool lookupExtraInfo = false) + protected async Task ProcessSingleMovie(SearchMovieViewModel viewMovie, bool lookupExtraInfo = false) { if (lookupExtraInfo && viewMovie.ImdbId.IsNullOrEmpty()) { @@ -214,7 +215,7 @@ namespace Ombi.Core.Engine // This requires the rules to be run first to populate the RequestId property await CheckForSubscription(viewMovie); - + return viewMovie; } @@ -228,7 +229,7 @@ namespace Ombi.Core.Engine } var request = await RequestService.MovieRequestService.GetAll() .AnyAsync(x => x.RequestedUserId.Equals(user.Id) && x.TheMovieDbId == viewModel.Id); - if (request) + if (request || viewModel.Available) { viewModel.ShowSubscribe = false; } diff --git a/src/Ombi.Core/Engine/MusicRequestEngine.cs b/src/Ombi.Core/Engine/MusicRequestEngine.cs index 8457de515..8094e79c4 100644 --- a/src/Ombi.Core/Engine/MusicRequestEngine.cs +++ b/src/Ombi.Core/Engine/MusicRequestEngine.cs @@ -69,6 +69,12 @@ namespace Ombi.Core.Engine }; } + if(album?.artist == null) + { + // Lookup the artist + //album.artist = await _lidarrApi.ArtistLookup(album.artist, s.ApiKey, s.FullUri); + } + var userDetails = await GetUser(); var requestModel = new AlbumRequest @@ -83,7 +89,7 @@ namespace Ombi.Core.Engine Title = album.title, Disk = album.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url, Cover = album.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url, - ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty, + ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty, // This needs to be populated to send to Lidarr for new requests RequestedByAlias = model.RequestedByAlias }; if (requestModel.Cover.IsNullOrEmpty()) diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index ddcc22d7b..b82335c80 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -7,6 +7,7 @@ using Ombi.Core.Models.Search; using Ombi.Helpers; using Ombi.Store.Entities; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Security.Principal; @@ -31,14 +32,13 @@ namespace Ombi.Core.Engine { public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, - ITvSender sender, IAuditRepository audit, IRepository rl, ISettingsService settings, ICacheService cache, + ITvSender sender, IRepository rl, ISettingsService settings, ICacheService cache, IRepository sub) : base(user, requestService, rule, manager, cache, settings, sub) { TvApi = tvApi; MovieDbApi = movApi; NotificationHelper = helper; TvSender = sender; - Audit = audit; _requestLog = rl; } @@ -46,7 +46,6 @@ namespace Ombi.Core.Engine private ITvMazeApi TvApi { get; } private IMovieDbApi MovieDbApi { get; } private ITvSender TvSender { get; } - private IAuditRepository Audit { get; } private readonly IRepository _requestLog; public async Task RequestTvShow(TvRequestViewModel tv) @@ -84,8 +83,6 @@ namespace Ombi.Core.Engine } } - await Audit.Record(AuditType.Added, AuditArea.TvRequest, $"Added Request {tvBuilder.ChildRequest.Title}", Username); - var existingRequest = await TvRepository.Get().FirstOrDefaultAsync(x => x.TvDbId == tv.TvDbId); if (existingRequest != null) { @@ -160,7 +157,7 @@ namespace Ombi.Core.Engine .ThenInclude(x => x.Episodes) .OrderByDescending(x => x.ChildRequests.Select(y => y.RequestedDate).FirstOrDefault()) .Skip(position).Take(count).ToListAsync(); - + } allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); }); @@ -225,6 +222,59 @@ namespace Ombi.Core.Engine } + public async Task> GetRequests(int count, int position, string sortProperty, string sortOrder) + { + var shouldHide = await HideFromOtherUsers(); + List allRequests; + if (shouldHide.Hide) + { + allRequests = await TvRepository.GetChild(shouldHide.UserId).ToListAsync(); + + // Filter out children + + FilterChildren(allRequests, shouldHide); + } + else + { + allRequests = await TvRepository.GetChild().ToListAsync(); + + } + + if (allRequests == null) + { + return new RequestsViewModel(); + } + + var total = allRequests.Count; + + + var prop = TypeDescriptor.GetProperties(typeof(ChildRequests)).Find(sortProperty, true); + + if (sortProperty.Contains('.')) + { + // This is a navigation property currently not supported + prop = TypeDescriptor.GetProperties(typeof(ChildRequests)).Find("Title", true); + //var properties = sortProperty.Split(new []{'.'}, StringSplitOptions.RemoveEmptyEntries); + //var firstProp = TypeDescriptor.GetProperties(typeof(MovieRequests)).Find(properties[0], true); + //var propType = firstProp.PropertyType; + //var secondProp = TypeDescriptor.GetProperties(propType).Find(properties[1], true); + } + allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) + ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() + : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); + allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); }); + + // Make sure we do not show duplicate child requests + allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); + + return new RequestsViewModel + { + Collection = allRequests, + Total = total, + }; + } + + public async Task> GetRequestsLite() { var shouldHide = await HideFromOtherUsers(); @@ -282,17 +332,22 @@ namespace Ombi.Core.Engine private static void FilterChildren(TvRequests t, HideResult shouldHide) { // Filter out children + FilterChildren(t.ChildRequests, shouldHide); + } - for (var j = 0; j < t.ChildRequests.Count; j++) + private static void FilterChildren(List t, HideResult shouldHide) + { + // Filter out children + + for (var j = 0; j < t.Count; j++) { - var child = t.ChildRequests[j]; + var child = t[j]; if (child.RequestedUserId != shouldHide.UserId) { - t.ChildRequests.RemoveAt(j); + t.RemoveAt(j); j--; } } - } public async Task> GetAllChldren(int tvId) @@ -351,7 +406,6 @@ namespace Ombi.Core.Engine public async Task UpdateTvRequest(TvRequests request) { - await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username); var allRequests = TvRepository.Get(); var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id); @@ -385,6 +439,7 @@ namespace Ombi.Core.Engine foreach (var ep in s.Episodes) { ep.Approved = true; + ep.Requested = true; } } @@ -393,7 +448,6 @@ namespace Ombi.Core.Engine if (request.Approved) { NotificationHelper.Notify(request, NotificationType.RequestApproved); - await Audit.Record(AuditType.Approved, AuditArea.TvRequest, $"Approved Request {request.Title}", Username); // Autosend await TvSender.Send(request); } @@ -425,9 +479,7 @@ namespace Ombi.Core.Engine public async Task UpdateChildRequest(ChildRequests request) { - await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username); - - await TvRepository.UpdateChild(request); + await TvRepository.UpdateChild(request); return request; } @@ -445,16 +497,14 @@ namespace Ombi.Core.Engine // Delete the parent TvRepository.Db.TvRequests.Remove(parent); } - await Audit.Record(AuditType.Deleted, AuditArea.TvRequest, $"Deleting Request {request.Title}", Username); - + await TvRepository.Db.SaveChangesAsync(); } public async Task RemoveTvRequest(int requestId) { var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId); - await Audit.Record(AuditType.Deleted, AuditArea.TvRequest, $"Deleting Request {request.Title}", Username); - await TvRepository.Delete(request); + await TvRepository.Delete(request); } public async Task UserHasRequest(string userId) @@ -569,7 +619,7 @@ namespace Ombi.Core.Engine return await AfterRequest(model.ChildRequests.FirstOrDefault()); } - private static List SortEpisodes(List items) + private static List SortEpisodes(List items) { foreach (var value in items) { @@ -606,7 +656,7 @@ namespace Ombi.Core.Engine var result = await TvSender.Send(model); if (result.Success) { - return new RequestEngineResult { Result = true, RequestId = model.Id}; + return new RequestEngineResult { Result = true, RequestId = model.Id }; } return new RequestEngineResult { @@ -659,10 +709,10 @@ namespace Ombi.Core.Engine DateTime oldestRequestedAt = await log.OrderBy(x => x.RequestDate) .Select(x => x.RequestDate) .FirstOrDefaultAsync(); - + return new RequestQuotaCountModel() { - HasLimit = true, + HasLimit = true, Limit = limit, Remaining = count, NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc), diff --git a/src/Ombi.Core/Engine/TvSearchEngine.cs b/src/Ombi.Core/Engine/TvSearchEngine.cs index d34a40deb..93e451f1d 100644 --- a/src/Ombi.Core/Engine/TvSearchEngine.cs +++ b/src/Ombi.Core/Engine/TvSearchEngine.cs @@ -21,6 +21,8 @@ using Ombi.Core.Authentication; using Ombi.Helpers; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; +using TraktApiSharp.Objects.Get.Shows; +using TraktApiSharp.Objects.Get.Shows.Common; namespace Ombi.Core.Engine { @@ -40,8 +42,8 @@ namespace Ombi.Core.Engine EmbyContentRepo = embyRepo; } - private ITvMazeApi TvMazeApi { get; } - private IMapper Mapper { get; } + protected ITvMazeApi TvMazeApi { get; } + protected IMapper Mapper { get; } private ISettingsService PlexSettings { get; } private ISettingsService EmbySettings { get; } private IPlexContentRepository PlexContentRepo { get; } @@ -99,7 +101,7 @@ namespace Ombi.Core.Engine { Url = e.url, Title = e.name, - AirDate = DateTime.Parse(e.airstamp ?? DateTime.MinValue.ToString()), + AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue, EpisodeNumber = e.number, }); @@ -112,7 +114,7 @@ namespace Ombi.Core.Engine { Url = e.url, Title = e.name, - AirDate = DateTime.Parse(e.airstamp ?? DateTime.MinValue.ToString()), + AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue, EpisodeNumber = e.number, }); } @@ -127,6 +129,19 @@ namespace Ombi.Core.Engine return processed; } + public async Task> Popular(int currentlyLoaded, int amountToLoad) + { + var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + var processed = ProcessResults(results); + return processed; + } + public async Task> Anticipated() { @@ -135,6 +150,19 @@ namespace Ombi.Core.Engine return processed; } + public async Task> Anticipated(int currentlyLoaded, int amountToLoad) + { + var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + var processed = ProcessResults(results); + return processed; + } + public async Task> MostWatches() { var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(null, ResultLimit), DateTime.Now.AddHours(12)); @@ -149,7 +177,33 @@ namespace Ombi.Core.Engine return processed; } - private IEnumerable ProcessResults(IEnumerable items) + public async Task> MostWatches(int currentlyLoaded, int amountToLoad) + { + var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await TraktApi.GetMostWatchesShows(null, pagesToLoad.Page, ResultLimit); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + var processed = ProcessResults(results); + return processed; + } + + public async Task> Trending(int currentlyLoaded, int amountToLoad) + { + var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + var processed = ProcessResults(results); + return processed; + } + + protected IEnumerable ProcessResults(IEnumerable items) { var retVal = new List(); foreach (var tvMazeSearch in items) @@ -159,7 +213,7 @@ namespace Ombi.Core.Engine return retVal; } - private SearchTvShowViewModel ProcessResult(T tvMazeSearch) + protected SearchTvShowViewModel ProcessResult(T tvMazeSearch) { return Mapper.Map(tvMazeSearch); } diff --git a/src/Ombi.Core/Engine/V2/CalendarEngine.cs b/src/Ombi.Core/Engine/V2/CalendarEngine.cs new file mode 100644 index 000000000..f4b63beb2 --- /dev/null +++ b/src/Ombi.Core/Engine/V2/CalendarEngine.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using Ombi.Api.Sonarr.Models; +using Ombi.Core.Authentication; +using Ombi.Core.Engine.Interfaces; +using Ombi.Core.Models.Search.V2; +using Ombi.Core.Rule.Interfaces; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Core.Engine.V2 +{ + public class CalendarEngine : BaseEngine, ICalendarEngine + { + public DateTime DaysAgo => DateTime.Now.AddDays(-90); + public DateTime DaysAhead => DateTime.Now.AddDays(90); + public CalendarEngine(IPrincipal user, OmbiUserManager um, IRuleEvaluator rules, IMovieRequestRepository movieRepo, + ITvRequestRepository tvRequestRepo) : base(user, um, rules) + { + _movieRepo = movieRepo; + _tvRepo = tvRequestRepo; + } + + private readonly IMovieRequestRepository _movieRepo; + private readonly ITvRequestRepository _tvRepo; + + public async Task> GetCalendarData() + { + var viewModel = new List(); + var movies = _movieRepo.GetAll().Where(x => + x.ReleaseDate > DaysAgo && x.ReleaseDate < DaysAhead); + var episodes = _tvRepo.GetChild().SelectMany(x => x.SeasonRequests.SelectMany(e => e.Episodes + .Where(w => w.AirDate > DaysAgo && w.AirDate < DaysAhead))); + foreach (var e in episodes) + { + viewModel.Add(new CalendarViewModel + { + Title = e.Title, + Start = e.AirDate.Date, + Type = RequestType.TvShow, + BackgroundColor = GetBackgroundColor(e), + ExtraParams = new List + { + new ExtraParams { Overview = e.Season?.ChildRequest?.ParentRequest?.Overview ?? string.Empty, ProviderId = e.Season?.ChildRequest?.ParentRequest?.TvDbId ?? 0} + } + }); + } + + foreach (var m in movies) + { + viewModel.Add(new CalendarViewModel + { + Title = m.Title, + Start = m.ReleaseDate.Date, + BackgroundColor = GetBackgroundColor(m), + Type = RequestType.Movie, + ExtraParams = new List + { + new ExtraParams { Overview = m.Overview, ProviderId = m.TheMovieDbId} + } + }); + } + + return viewModel; + } + + private string GetBackgroundColor(BaseRequest req) + { + if (req.Available) + { + return "#469c83"; + } + + if (!req.Available) + { + if (req.Denied ?? false) + { + return "red"; + } + if (req.Approved) + { + // We are approved state + return "blue"; + } + + if (!req.Approved) + { + // Processing + return "teal"; + } + } + + return "gray"; + } + + private string GetBackgroundColor(EpisodeRequests req) + { + if (req.Available) + { + return "#469c83"; + } + + if (!req.Available) + { + if (req.Approved) + { + // We are approved state + return "blue"; + } + + if (!req.Approved) + { + // Processing + return "teal"; + } + + } + + return "gray"; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/V2/ICalendarEngine.cs b/src/Ombi.Core/Engine/V2/ICalendarEngine.cs new file mode 100644 index 000000000..1a97d9227 --- /dev/null +++ b/src/Ombi.Core/Engine/V2/ICalendarEngine.cs @@ -0,0 +1,11 @@ +using Ombi.Core.Models.Search.V2; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Ombi.Core.Engine.V2 +{ + public interface ICalendarEngine + { + Task> GetCalendarData(); + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index 1c77f8607..cb3162900 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -4,8 +4,10 @@ using Microsoft.Extensions.Logging; using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Authentication; +using Ombi.Core.Engine.Interfaces; using Ombi.Core.Models.Requests; using Ombi.Core.Models.Search; +using Ombi.Core.Models.Search.V2; using Ombi.Core.Rule.Interfaces; using Ombi.Core.Settings; using Ombi.Helpers; @@ -17,9 +19,8 @@ using System.Collections.Generic; using System.Linq; using System.Security.Principal; using System.Threading.Tasks; -using Ombi.Core.Models.Search.V2; -namespace Ombi.Core.Engine +namespace Ombi.Core.Engine.V2 { public class MovieSearchEngineV2 : BaseMediaEngine, IMovieEngineV2 { @@ -45,6 +46,198 @@ namespace Ombi.Core.Engine return await ProcessSingleMovie(movieInfo); } + public async Task GetCollection(int collectionId, string langCode = null) + { + langCode = await DefaultLanguageCode(langCode); + var collections = await MovieApi.GetCollection(langCode, collectionId); + + var c = await ProcessCollection(collections); + c.Collection = c.Collection.OrderBy(x => x.ReleaseDate).ToList(); + return c; + } + + public async Task GetTvDbId(int theMovieDbId) + { + var result = await MovieApi.GetTvExternals(theMovieDbId); + return result.tvdb_id; + } + + /// + /// Get similar movies to the id passed in + /// + /// + /// + public async Task> SimilarMovies(int theMovieDbId, string langCode) + { + langCode = await DefaultLanguageCode(langCode); + var result = await MovieApi.SimilarMovies(theMovieDbId, langCode); + if (result != null) + { + Logger.LogDebug("Search Result: {result}", result); + return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API + } + return null; + } + + /// + /// Gets popular movies. + /// + /// + public async Task> PopularMovies() + { + + var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => + { + var langCode = await DefaultLanguageCode(null); + return await MovieApi.PopularMovies(langCode); + }, DateTime.Now.AddHours(12)); + if (result != null) + { + return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API + } + return null; + } + + + private const int _theMovieDbMaxPageItems = 20; + + /// + /// Gets popular movies by paging + /// + /// + public async Task> PopularMovies(int currentlyLoaded, int toLoad) + { + var langCode = await DefaultLanguageCode(null); + + var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await MovieApi.PopularMovies(langCode, pagesToLoad.Page); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + /// + /// Gets top rated movies. + /// + /// + public async Task> TopRatedMovies() + { + var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => + { + var langCode = await DefaultLanguageCode(null); + return await MovieApi.TopRated(langCode); + }, DateTime.Now.AddHours(12)); + if (result != null) + { + return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API + } + return null; + } + + public async Task> TopRatedMovies(int currentPosition, int amountToLoad) + { + var langCode = await DefaultLanguageCode(null); + + var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await MovieApi.TopRated(langCode, pagesToLoad.Page); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + public async Task> NowPlayingMovies(int currentPosition, int amountToLoad) + { + var langCode = await DefaultLanguageCode(null); + + var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await MovieApi.NowPlaying(langCode, pagesToLoad.Page); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + + /// + /// Gets upcoming movies. + /// + /// + public async Task> UpcomingMovies() + { + var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => + { + var langCode = await DefaultLanguageCode(null); + return await MovieApi.Upcoming(langCode); + }, DateTime.Now.AddHours(12)); + if (result != null) + { + Logger.LogDebug("Search Result: {result}", result); + return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API + } + return null; + } + + public async Task> UpcomingMovies(int currentPosition, int amountToLoad) + { + var langCode = await DefaultLanguageCode(null); + + var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await MovieApi.Upcoming(langCode, pagesToLoad.Page); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + /// + /// Gets now playing movies. + /// + /// + public async Task> NowPlayingMovies() + { + var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => + { + var langCode = await DefaultLanguageCode(null); + return await MovieApi.NowPlaying(langCode); + }, DateTime.Now.AddHours(12)); + if (result != null) + { + return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API + } + return null; + } + + protected async Task> TransformMovieResultsToResponse( + IEnumerable movies) + { + var viewMovies = new List(); + foreach (var movie in movies) + { + viewMovies.Add(await ProcessSingleMovie(movie)); + } + return viewMovies; + } + + private async Task ProcessSingleMovie(MovieSearchResult movie) + { + var viewMovie = Mapper.Map(movie); + return await ProcessSingleMovie(viewMovie); + } + private async Task ProcessSingleMovie(FullMovieInfo movie) { var viewMovie = Mapper.Map(movie); @@ -65,6 +258,50 @@ namespace Ombi.Core.Engine return mapped; } + + private async Task ProcessCollection(Collections collection) + { + var viewMovie = Mapper.Map(collection); + foreach (var movie in viewMovie.Collection) + { + var mappedMovie = Mapper.Map(movie); + await RunSearchRules(mappedMovie); + + // This requires the rules to be run first to populate the RequestId property + await CheckForSubscription(mappedMovie); + var mapped = Mapper.Map(movie); + + mapped.Available = movie.Available; + mapped.RequestId = movie.RequestId; + mapped.Requested = movie.Requested; + mapped.PlexUrl = movie.PlexUrl; + mapped.EmbyUrl = movie.EmbyUrl; + mapped.Subscribed = movie.Subscribed; + mapped.ShowSubscribe = movie.ShowSubscribe; + mapped.ReleaseDate = movie.ReleaseDate; + } + return viewMovie; + } + + private async Task ProcessSingleMovie(SearchMovieViewModel viewMovie) + { + if (viewMovie.ImdbId.IsNullOrEmpty()) + { + var showInfo = await MovieApi.GetMovieInformation(viewMovie.Id); + viewMovie.Id = showInfo.Id; // TheMovieDbId + viewMovie.ImdbId = showInfo.ImdbId; + } + + viewMovie.TheMovieDbId = viewMovie.Id.ToString(); + + await RunSearchRules(viewMovie); + + // This requires the rules to be run first to populate the RequestId property + await CheckForSubscription(viewMovie); + + return viewMovie; + } + private async Task CheckForSubscription(SearchViewModel viewModel) { // Check if this user requested it diff --git a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs new file mode 100644 index 000000000..4dd20926b --- /dev/null +++ b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs @@ -0,0 +1,154 @@ +using AutoMapper; + +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using TraktApiSharp.Objects.Get.Shows; + +using Ombi.Core.Rule.Interfaces; +using Ombi.Store.Repository.Requests; +using Ombi.Core.Authentication; +using Ombi.Helpers; +using Ombi.Settings.Settings.Models; +using Ombi.Store.Entities; +using Ombi.Api.Trakt; +using Ombi.Api.TvMaze; +using Ombi.Core.Models.Requests; +using Ombi.Core.Models.Search; +using Ombi.Core.Models.Search.V2; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Store.Repository; + +namespace Ombi.Core.Engine.V2 +{ + public class TvSearchEngineV2 : BaseMediaEngine, ITVSearchEngineV2 + { + public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService plexSettings, + ISettingsService embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, + ICacheService memCache, ISettingsService s, IRepository sub) + : base(identity, service, r, um, memCache, s, sub) + { + TvMazeApi = tvMaze; + Mapper = mapper; + PlexSettings = plexSettings; + EmbySettings = embySettings; + PlexContentRepo = repo; + TraktApi = trakt; + EmbyContentRepo = embyRepo; + } + + private ITvMazeApi TvMazeApi { get; } + private IMapper Mapper { get; } + private ISettingsService PlexSettings { get; } + private ISettingsService EmbySettings { get; } + private IPlexContentRepository PlexContentRepo { get; } + private IEmbyContentRepository EmbyContentRepo { get; } + private ITraktApi TraktApi { get; } + + + public async Task GetShowInformation(int tvdbid) + { + var tvdbshow = await TvMazeApi.ShowLookupByTheTvDbId(tvdbid); + var show = await TvMazeApi.GetTvFullInformation(tvdbshow.id); + if (show == null) + { + // We don't have enough information + return null; + } + + // Setup the task so we can get the data later on if we have a IMDBID + Task traktInfoTask = new Task(() => null); + if (show.externals?.imdb.HasValue() ?? false) + { + traktInfoTask = TraktApi.GetTvExtendedInfo(show.externals?.imdb); + } + + var mapped = Mapper.Map(show); + + foreach (var e in show._embedded.episodes) + { + var season = mapped.SeasonRequests.FirstOrDefault(x => x.SeasonNumber == e.season); + if (season == null) + { + var newSeason = new SeasonRequests + { + SeasonNumber = e.season, + Episodes = new List() + }; + newSeason.Episodes.Add(new EpisodeRequests + { + Url = e.url, + Title = e.name, + AirDate = e.airstamp, + EpisodeNumber = e.number, + + }); + mapped.SeasonRequests.Add(newSeason); + } + else + { + // We already have the season, so just add the episode + season.Episodes.Add(new EpisodeRequests + { + Url = e.url, + Title = e.name, + AirDate = e.airstamp, + EpisodeNumber = e.number, + }); + } + } + return await ProcessResult(mapped, traktInfoTask); + } + + private IEnumerable ProcessResults(IEnumerable items) + { + var retVal = new List(); + foreach (var tvMazeSearch in items) + { + retVal.Add(ProcessResult(tvMazeSearch)); + } + return retVal; + } + + private SearchTvShowViewModel ProcessResult(T tvMazeSearch) + { + return Mapper.Map(tvMazeSearch); + } + + private async Task ProcessResult(SearchFullInfoTvShowViewModel item, Task showInfoTask) + { + item.TheTvDbId = item.Id.ToString(); + + var oldModel = Mapper.Map(item); + await RunSearchRules(oldModel); + + item.Available = oldModel.Available; + item.FullyAvailable = oldModel.FullyAvailable; + item.PartlyAvailable = oldModel.PartlyAvailable; + item.Requested = oldModel.Requested; + item.Available = oldModel.Available; + item.Approved = oldModel.Approved; + item.SeasonRequests = oldModel.SeasonRequests; + item.RequestId = oldModel.RequestId; + + return await GetExtraInfo(showInfoTask, item); + } + + private async Task GetExtraInfo(Task showInfoTask, SearchFullInfoTvShowViewModel model) + { + var result = await showInfoTask; + if(result == null) + { + return model; + } + + model.Trailer = result.Trailer; + model.Certification = result.Certification; + model.Homepage = result.Homepage; + + return model; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/Search/V2/CalendarViewModel.cs b/src/Ombi.Core/Models/Search/V2/CalendarViewModel.cs new file mode 100644 index 000000000..3cdedce7c --- /dev/null +++ b/src/Ombi.Core/Models/Search/V2/CalendarViewModel.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.Search.V2 +{ + public class CalendarViewModel + { + public string Title { get; set; } + public DateTime Start { get; set; } + public string BackgroundColor { get; set; } + public RequestType Type { get; set; } + public List ExtraParams { get; set; } + + public string BorderColor + { + get + { + switch (Type) + { + case RequestType.TvShow: + return "#ff0000"; + case RequestType.Movie: + return "#0d5a3e"; + case RequestType.Album: + return "#797979"; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + public class ExtraParams + { + public int ProviderId { get; set; } + public string Overview { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs b/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs new file mode 100644 index 000000000..ace5ce76c --- /dev/null +++ b/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.Search.V2 +{ + public class MovieCollectionsViewModel + { + public string Name { get; set; } + public string Overview { get; set; } + public List Collection { get; set; } + } + + public class MovieCollection : SearchViewModel + { + public int Id { get; set; } + public string Overview { get; set; } + public string PosterPath { get; set; } + public string Title { get; set; } + public DateTime ReleaseDate { get; set; } + + + public override RequestType Type => RequestType.Movie; + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/Search/V2/MovieFullInfoViewModel.cs b/src/Ombi.Core/Models/Search/V2/MovieFullInfoViewModel.cs index 97496f35f..f1d8665b0 100644 --- a/src/Ombi.Core/Models/Search/V2/MovieFullInfoViewModel.cs +++ b/src/Ombi.Core/Models/Search/V2/MovieFullInfoViewModel.cs @@ -8,6 +8,7 @@ namespace Ombi.Core.Models.Search.V2 public class MovieFullInfoViewModel : SearchViewModel { public bool Adult { get; set; } + public CollectionsViewModel BelongsToCollection { get; set; } public string BackdropPath { get; set; } public string OriginalLanguage { get; set; } public int Budget { get; set; } @@ -39,8 +40,26 @@ namespace Ombi.Core.Models.Search.V2 public Similar Similar { get; set; } public Recommendations Recommendations { get; set; } public ExternalIds ExternalIds { get; set; } + public Keywords Keywords { get; set; } + } + public class Keywords + { + public List KeywordsValue { get; set; } } + public class KeywordsValue + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class CollectionsViewModel + { + public int Id { get; set; } + public string Name { get; set; } + public string PosterPath { get; set; } + public string BackdropPath { get; set; } + } public class ExternalIds { public string ImdbId { get; set; } diff --git a/src/Ombi.Core/Models/Search/V2/SearchFullInfoTvShowViewModel.cs b/src/Ombi.Core/Models/Search/V2/SearchFullInfoTvShowViewModel.cs new file mode 100644 index 000000000..8533bb2d7 --- /dev/null +++ b/src/Ombi.Core/Models/Search/V2/SearchFullInfoTvShowViewModel.cs @@ -0,0 +1,114 @@ +using Ombi.Store.Repository.Requests; +using System.Collections.Generic; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.Search.V2 +{ + public class SearchFullInfoTvShowViewModel : SearchViewModel + { + public string Title { get; set; } + public List Aliases { get; set; } + public string Banner { get; set; } + public int SeriesId { get; set; } + public string Status { get; set; } + public string FirstAired { get; set; } + public string NetworkId { get; set; } + public string Runtime { get; set; } + public List Genre { get; set; } + public string Overview { get; set; } + public int LastUpdated { get; set; } + public string AirsDayOfWeek { get; set; } + public string AirsTime { get; set; } + public string Rating { get; set; } + public int SiteRating { get; set; } + public NetworkViewModel Network { get; set; } + public Images Images { get; set; } + public List Cast { get; set; } + public List Crew { get; set; } + public string Certification { get; set; } + + /// + /// This is used from the Trakt API + /// + /// + /// The trailer. + /// + public string Trailer { get; set; } + + /// + /// This is used from the Trakt API + /// + /// + /// The trailer. + /// + public string Homepage { get; set; } + + public List SeasonRequests { get; set; } = new List(); + + /// + /// If we are requesting the entire series + /// + public bool RequestAll { get; set; } + + public bool FirstSeason { get; set; } + public bool LatestSeason { get; set; } + + /// + /// This is where we have EVERY Episode for that series + /// + public bool FullyAvailable { get; set; } + // We only have some episodes + public bool PartlyAvailable { get; set; } + public override RequestType Type => RequestType.TvShow; + } + + public class NetworkViewModel + { + public int Id { get; set; } + public string Name { get; set; } + public Country Country { get; set; } + } + + public class Country + { + public string Name { get; set; } + public string Code { get; set; } + public string Timezone { get; set; } + } + + public class Images + { + public string Medium { get; set; } + public string Original { get; set; } + } + + public class CastViewModel + { + public PersonViewModel Person { get; set; } + public CharacterViewModel Character { get; set; } + public bool Self { get; set; } + public bool Voice { get; set; } + } + + public class PersonViewModel + { + public int Id { get; set; } + public string Url { get; set; } + public string Name { get; set; } + public Images Image { get; set; } + } + + public class CharacterViewModel + { + public int Id { get; set; } + public string Url { get; set; } + public string Name { get; set; } + public Images Image { get; set; } + } + + public class CrewViewModel + { + public string Type { get; set; } + public PersonViewModel Person { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/UI/GotifyNotificationViewModel.cs b/src/Ombi.Core/Models/UI/GotifyNotificationViewModel.cs new file mode 100644 index 000000000..93ce66724 --- /dev/null +++ b/src/Ombi.Core/Models/UI/GotifyNotificationViewModel.cs @@ -0,0 +1,23 @@ + +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class GotifyNotificationViewModel : GotifySettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public List NotificationTemplates { get; set; } + + } +} diff --git a/src/Ombi.Core/Ombi.Core.csproj b/src/Ombi.Core/Ombi.Core.csproj index f91cb053e..837a2ab57 100644 --- a/src/Ombi.Core/Ombi.Core.csproj +++ b/src/Ombi.Core/Ombi.Core.csproj @@ -11,10 +11,9 @@ - + - - + diff --git a/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs b/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs index a55868db8..685f02b54 100644 --- a/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs @@ -1,5 +1,7 @@ using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Ombi.Core.Authentication; using Ombi.Core.Models.Requests; using Ombi.Core.Rule.Interfaces; using Ombi.Helpers; @@ -10,28 +12,31 @@ namespace Ombi.Core.Rule.Rules.Request { public class AutoApproveRule : BaseRequestRule, IRules { - public AutoApproveRule(IPrincipal principal) + public AutoApproveRule(IPrincipal principal, OmbiUserManager um) { User = principal; + _manager = um; } private IPrincipal User { get; } + private readonly OmbiUserManager _manager; - public Task Execute(BaseRequest obj) + public async Task Execute(BaseRequest obj) { - if (User.IsInRole(OmbiRoles.Admin)) + var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name); + if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin)) { obj.Approved = true; - return Task.FromResult(Success()); + return Success(); } - if (obj.RequestType == RequestType.Movie && User.IsInRole(OmbiRoles.AutoApproveMovie)) + if (obj.RequestType == RequestType.Movie && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMovie)) obj.Approved = true; - if (obj.RequestType == RequestType.TvShow && User.IsInRole(OmbiRoles.AutoApproveTv)) + if (obj.RequestType == RequestType.TvShow && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveTv)) obj.Approved = true; - if (obj.RequestType == RequestType.Album && User.IsInRole(OmbiRoles.AutoApproveMusic)) + if (obj.RequestType == RequestType.Album && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMusic)) obj.Approved = true; - return Task.FromResult(Success()); // We don't really care, we just don't set the obj to approve + return Success(); // We don't really care, we just don't set the obj to approve } } } \ No newline at end of file diff --git a/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs index 1cdf03955..a2c70fcc5 100644 --- a/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs @@ -1,46 +1,52 @@ -using Ombi.Store.Entities; +using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Ombi.Core.Authentication; using Ombi.Core.Rule.Interfaces; using Ombi.Helpers; +using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; -namespace Ombi.Core.Rule.Rules +namespace Ombi.Core.Rule.Rules.Request { public class CanRequestRule : BaseRequestRule, IRules { - public CanRequestRule(IPrincipal principal) + public CanRequestRule(IPrincipal principal, OmbiUserManager manager) { User = principal; + _manager = manager; } private IPrincipal User { get; } + private readonly OmbiUserManager _manager; - public Task Execute(BaseRequest obj) + public async Task Execute(BaseRequest obj) { - if (User.IsInRole(OmbiRoles.Admin)) - return Task.FromResult(Success()); + var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name); + if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin)) + return Success(); if (obj.RequestType == RequestType.Movie) { - if (User.IsInRole(OmbiRoles.RequestMovie) || User.IsInRole(OmbiRoles.AutoApproveMovie)) - return Task.FromResult(Success()); - return Task.FromResult(Fail("You do not have permissions to Request a Movie")); + if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestMovie) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMovie)) + return Success(); + return Fail("You do not have permissions to Request a Movie"); } if (obj.RequestType == RequestType.TvShow) { - if (User.IsInRole(OmbiRoles.RequestTv) || User.IsInRole(OmbiRoles.AutoApproveTv)) - return Task.FromResult(Success()); + if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestTv) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveTv)) + return Success(); } if (obj.RequestType == RequestType.Album) { - if (User.IsInRole(OmbiRoles.RequestMusic) || User.IsInRole(OmbiRoles.AutoApproveMusic)) - return Task.FromResult(Success()); + if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestMusic) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMusic)) + return Success(); } - return Task.FromResult(Fail("You do not have permissions to Request a TV Show")); + return Fail("You do not have permissions to Request a TV Show"); } } } \ No newline at end of file diff --git a/src/Ombi.Core/Rule/Rules/Request/ExistingMovieRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/ExistingMovieRequestRule.cs index d48245e9b..bc580e9ea 100644 --- a/src/Ombi.Core/Rule/Rules/Request/ExistingMovieRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/ExistingMovieRequestRule.cs @@ -1,6 +1,8 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Core.Rule.Interfaces; +using Ombi.Helpers; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; @@ -28,8 +30,24 @@ namespace Ombi.Core.Rule.Rules.Request { var movie = (MovieRequests) obj; var movieRequests = Movie.GetAll(); + var found = false; var existing = await movieRequests.FirstOrDefaultAsync(x => x.TheMovieDbId == movie.TheMovieDbId); if (existing != null) // Do we already have a request for this? + { + found = true; + } + + if (!found && movie.ImdbId.HasValue()) + { + // Let's check imdbid + existing = await movieRequests.FirstOrDefaultAsync(x => + x.ImdbId.Equals(movie.ImdbId, StringComparison.CurrentCultureIgnoreCase)); + if (existing != null) + { + found = true; + } + } + if(found) { return Fail($"\"{obj.Title}\" has already been requested"); } diff --git a/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs index 5d7658c83..0d887063e 100644 --- a/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs @@ -32,7 +32,7 @@ namespace Ombi.Core.Rule.Rules.Request var tvContent = _plexContent.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Show); // We need to do a check on the TVDBId - var anyTvDbMatches = await tvContent.Include(x => x.Episodes).FirstOrDefaultAsync(x => x.HasTvDb && x.TvDbId.Equals(tvRequest.Id.ToString())); // the Id on the child is the tvdbid at this point + var anyTvDbMatches = await tvContent.Include(x => x.Episodes).FirstOrDefaultAsync(x => x.HasTvDb && x.TvDbId.Equals(tvRequest.Id.ToString(), StringComparison.InvariantCultureIgnoreCase)); // the Id on the child is the tvdbid at this point if (anyTvDbMatches == null) { // So we do not have a TVDB Id, that really sucks. @@ -42,7 +42,7 @@ namespace Ombi.Core.Rule.Rules.Request && x.ReleaseYear == tvRequest.ReleaseYear.Year.ToString()); if (titleAndYearMatch != null) { - // We have a match! Suprise Motherfucker + // We have a match! Surprise Motherfucker return CheckExistingContent(tvRequest, titleAndYearMatch); } diff --git a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs index 428de9ce5..24faa3a97 100644 --- a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs +++ b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; +using Microsoft.Extensions.Logging; using Ombi.Core.Models.Search; using Ombi.Store.Entities; using Ombi.Store.Repository.Requests; @@ -13,6 +13,18 @@ namespace Ombi.Core.Rule.Rules.Search { public static void CheckForUnairedEpisodes(SearchTvShowViewModel search) { + foreach (var season in search.SeasonRequests) + { + // If we have all the episodes for this season, then this season is available + if (season.Episodes.All(x => x.Available)) + { + season.SeasonAvailable = true; + } + } + if(search.SeasonRequests.Any(x => x.Episodes.Any(e => e.Available))) + { + search.PartlyAvailable = true; + } if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available))) { search.FullyAvailable = true; @@ -24,7 +36,7 @@ namespace Ombi.Core.Rule.Rules.Search if (!airedButNotAvailable) { var unairedEpisodes = search.SeasonRequests.Any(x => - x.Episodes.Any(c => !c.Available && c.AirDate > DateTime.Now.Date)); + x.Episodes.Any(c => !c.Available && c.AirDate > DateTime.Now.Date || c.AirDate != DateTime.MinValue)); if (unairedEpisodes) { search.FullyAvailable = true; @@ -34,28 +46,36 @@ namespace Ombi.Core.Rule.Rules.Search } public static async Task SingleEpisodeCheck(bool useImdb, IQueryable allEpisodes, EpisodeRequests episode, - SeasonRequests season, PlexServerContent item, bool useTheMovieDb, bool useTvDb) + SeasonRequests season, PlexServerContent item, bool useTheMovieDb, bool useTvDb, ILogger log) { PlexEpisode epExists = null; - if (useImdb) + try { - epExists = await allEpisodes.FirstOrDefaultAsync(x => - x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.ImdbId == item.ImdbId.ToString()); - } - if (useTheMovieDb) - { - epExists = await allEpisodes.FirstOrDefaultAsync(x => - x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.TheMovieDbId == item.TheMovieDbId.ToString()); - } + if (useImdb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.ImdbId == item.ImdbId); + } - if (useTvDb) + if (useTheMovieDb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.TheMovieDbId == item.TheMovieDbId); + } + + if (useTvDb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.TvDbId == item.TvDbId); + } + } + catch (Exception e) { - epExists = await allEpisodes.FirstOrDefaultAsync(x => - x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.TvDbId == item.TvDbId.ToString()); + log.LogError(e, "Exception thrown when attempting to check if something is available"); } if (epExists != null) @@ -71,21 +91,21 @@ namespace Ombi.Core.Rule.Rules.Search { epExists = await allEpisodes.FirstOrDefaultAsync(x => x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.ImdbId == item.ImdbId.ToString()); + x.Series.ImdbId == item.ImdbId); } if (useTheMovieDb) { epExists = await allEpisodes.FirstOrDefaultAsync(x => x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.TheMovieDbId == item.TheMovieDbId.ToString()); + x.Series.TheMovieDbId == item.TheMovieDbId); } if (useTvDb) { epExists = await allEpisodes.FirstOrDefaultAsync(x => x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.TvDbId == item.TvDbId.ToString()); + x.Series.TvDbId == item.TvDbId); } if (epExists != null) diff --git a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs index 3171c6ada..8fcc92c1f 100644 --- a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs @@ -3,6 +3,8 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Core.Models.Search; using Ombi.Core.Rule.Interfaces; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Store.Entities; using Ombi.Store.Repository; @@ -11,12 +13,14 @@ namespace Ombi.Core.Rule.Rules.Search { public class EmbyAvailabilityRule : BaseSearchRule, IRules { - public EmbyAvailabilityRule(IEmbyContentRepository repo) + public EmbyAvailabilityRule(IEmbyContentRepository repo, ISettingsService s) { EmbyContentRepository = repo; + EmbySettings = s; } private IEmbyContentRepository EmbyContentRepository { get; } + private ISettingsService EmbySettings { get; } public async Task Execute(SearchViewModel obj) { @@ -60,7 +64,19 @@ namespace Ombi.Core.Rule.Rules.Search if (item != null) { obj.Available = true; - obj.EmbyUrl = item.Url; + var s = await EmbySettings.GetSettingsAsync(); + if (s.Enable) + { + var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); + if ((server?.ServerHostname ?? string.Empty).HasValue()) + { + obj.EmbyUrl = $"{server.ServerHostname}#!/itemdetails.html?id={item.EmbyId}"; + } + else + { + obj.EmbyUrl = $"https://app.emby.media/#!/itemdetails.html?id={item.EmbyId}"; + } + } if (obj.Type == RequestType.TvShow) { diff --git a/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs b/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs index 965fcdfaf..d56de74ac 100644 --- a/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Core.Models.Search; @@ -29,7 +30,6 @@ namespace Ombi.Core.Rule.Rules.Search var movieRequests = await Movie.GetRequestAsync(obj.Id); if (movieRequests != null) // Do we already have a request for this? { - obj.Requested = true; obj.RequestId = movieRequests.Id; obj.Approved = movieRequests.Approved; @@ -41,22 +41,11 @@ namespace Ombi.Core.Rule.Rules.Search } if (obj.Type == RequestType.TvShow) { - //var tvRequests = Tv.GetRequest(obj.Id); - //if (tvRequests != null) // Do we already have a request for this? - //{ - - // obj.Requested = true; - // obj.Approved = tvRequests.ChildRequests.Any(x => x.Approved); - // obj.Available = tvRequests.ChildRequests.Any(x => x.Available); - - // return Task.FromResult(Success()); - //} - var request = (SearchTvShowViewModel)obj; var tvRequests = Tv.GetRequest(obj.Id); if (tvRequests != null) // Do we already have a request for this? { - + request.RequestId = tvRequests.Id; request.Requested = true; request.Approved = tvRequests.ChildRequests.Any(x => x.Approved); @@ -87,11 +76,11 @@ namespace Ombi.Core.Rule.Rules.Search } } - if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Available))) + if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Available && e.AirDate > DateTime.MinValue))) { request.FullyAvailable = true; } - if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.Any(e => e.Available))) + if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.Any(e => e.Available && e.AirDate > DateTime.MinValue))) { request.PartlyAvailable = true; } diff --git a/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs index 7f79e4165..2a239d1d3 100644 --- a/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Ombi.Core.Models.Search; using Ombi.Core.Rule.Interfaces; using Ombi.Helpers; @@ -10,12 +11,14 @@ namespace Ombi.Core.Rule.Rules.Search { public class PlexAvailabilityRule : BaseSearchRule, IRules { - public PlexAvailabilityRule(IPlexContentRepository repo) + public PlexAvailabilityRule(IPlexContentRepository repo, ILogger log) { PlexContentRepository = repo; + Log = log; } private IPlexContentRepository PlexContentRepository { get; } + private ILogger Log { get; } public async Task Execute(SearchViewModel obj) { @@ -72,7 +75,7 @@ namespace Ombi.Core.Rule.Rules.Search { foreach (var episode in season.Episodes) { - await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb); + await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb, Log); } } diff --git a/src/Ombi.Core/Senders/MovieSender.cs b/src/Ombi.Core/Senders/MovieSender.cs index 9124eb9c9..567df43b5 100644 --- a/src/Ombi.Core/Senders/MovieSender.cs +++ b/src/Ombi.Core/Senders/MovieSender.cs @@ -49,7 +49,6 @@ namespace Ombi.Core.Senders { try { - var cpSettings = await CouchPotatoSettings.GetSettingsAsync(); //var watcherSettings = await WatcherSettings.GetSettingsAsync(); var radarrSettings = await RadarrSettings.GetSettingsAsync(); @@ -76,7 +75,7 @@ namespace Ombi.Core.Senders } catch (Exception e) { - Log.LogError(e, "Error when seing movie to DVR app, added to the request queue"); + Log.LogError(e, "Error when sending movie to DVR app, added to the request queue"); // Check if already in request quee var existingQueue = await _requestQueuRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id); diff --git a/src/Ombi.Core/Senders/TvSender.cs b/src/Ombi.Core/Senders/TvSender.cs index f0784a4ca..9a25ca9c0 100644 --- a/src/Ombi.Core/Senders/TvSender.cs +++ b/src/Ombi.Core/Senders/TvSender.cs @@ -16,6 +16,7 @@ using Ombi.Settings.Settings.Models.External; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; +using Remotion.Linq.Parsing.Structure.IntermediateModel; namespace Ombi.Core.Senders { @@ -57,7 +58,7 @@ namespace Ombi.Core.Senders var sonarr = await SonarrSettings.GetSettingsAsync(); if (sonarr.Enabled) { - var result = await SendToSonarr(model); + var result = await SendToSonarr(model, sonarr); if (result != null) { return new SenderResult @@ -109,7 +110,7 @@ namespace Ombi.Core.Senders catch (Exception e) { Logger.LogError(e, "Exception thrown when sending a movie to DVR app, added to the request queue"); - // Check if already in request quee + // Check if already in request queue var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id); if (existingQueue != null) { @@ -134,7 +135,7 @@ namespace Ombi.Core.Senders return new SenderResult { Success = false, - Message = "Something wen't wrong!" + Message = "Something went wrong!" }; } @@ -150,13 +151,8 @@ namespace Ombi.Core.Senders /// /// /// - public async Task SendToSonarr(ChildRequests model) + public async Task SendToSonarr(ChildRequests model, SonarrSettings s) { - var s = await SonarrSettings.GetSettingsAsync(); - if (!s.Enabled) - { - return null; - } if (string.IsNullOrEmpty(s.ApiKey)) { return null; @@ -319,10 +315,19 @@ namespace Ombi.Core.Senders foreach (var season in model.SeasonRequests) { - var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber); - var sonarrEpCount = sonarrSeason.Count(); + var sonarrEpisodeList = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber).ToList(); + var sonarrEpCount = sonarrEpisodeList.Count; var ourRequestCount = season.Episodes.Count; + var ourEpisodes = season.Episodes.Select(x => x.EpisodeNumber).ToList(); + var unairedEpisodes = sonarrEpisodeList.Where(x => x.airDateUtc > DateTime.UtcNow).Select(x => x.episodeNumber).ToList(); + + //// Check if we have requested all the latest episodes, if we have then monitor + //// NOTE, not sure if needed since ombi ui displays future episodes anyway... + //ourEpisodes.AddRange(unairedEpisodes); + //var distinctEpisodes = ourEpisodes.Distinct().ToList(); + //var missingEpisodes = Enumerable.Range(distinctEpisodes.Min(), distinctEpisodes.Count).Except(distinctEpisodes); + var existingSeason = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); if (existingSeason == null) @@ -332,7 +337,7 @@ namespace Ombi.Core.Senders } - if (sonarrEpCount == ourRequestCount) + if (sonarrEpCount == ourRequestCount /*|| !missingEpisodes.Any()*/) { // We have the same amount of requests as all of the episodes in the season. diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 1bea848ff..d84cab6a7 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -33,6 +33,7 @@ using Ombi.Api.CouchPotato; using Ombi.Api.DogNzb; using Ombi.Api.FanartTv; using Ombi.Api.Github; +using Ombi.Api.Gotify; using Ombi.Api.Lidarr; using Ombi.Api.Mattermost; using Ombi.Api.Notifications; @@ -51,16 +52,15 @@ using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Sonarr; using Ombi.Store.Repository.Requests; using Ombi.Updater; -using PlexContentCacher = Ombi.Schedule.Jobs.Plex; using Ombi.Api.Telegram; using Ombi.Core.Authentication; +using Ombi.Core.Engine.Demo; using Ombi.Core.Engine.V2; using Ombi.Core.Processor; using Ombi.Schedule.Jobs.Lidarr; using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Schedule.Jobs.SickRage; using Ombi.Schedule.Processor; -using Ombi.Store.Entities; namespace Ombi.DependencyInjection { @@ -95,12 +95,16 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterEnginesV2(this IServiceCollection services) { services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterHttp(this IServiceCollection services) @@ -127,6 +131,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -139,28 +144,28 @@ namespace Ombi.DependencyInjection } public static void RegisterStore(this IServiceCollection services) { - services.AddEntityFrameworkSqlite().AddDbContext(); - services.AddEntityFrameworkSqlite().AddDbContext(); - services.AddEntityFrameworkSqlite().AddDbContext(); + services.AddDbContext(); + services.AddDbContext(); + services.AddDbContext(); services.AddScoped(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 services.AddScoped(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 services.AddScoped(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(typeof(ISettingsService<>), typeof(SettingsService<>)); - services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); - services.AddTransient(typeof(IExternalRepository<>), typeof(ExternalRepository<>)); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(typeof(ISettingsService<>), typeof(SettingsService<>)); + services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + services.AddScoped(typeof(IExternalRepository<>), typeof(ExternalRepository<>)); } public static void RegisterServices(this IServiceCollection services) { @@ -168,7 +173,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); @@ -177,6 +182,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.Helpers.Tests/EmbyHelperTests.cs b/src/Ombi.Helpers.Tests/EmbyHelperTests.cs new file mode 100644 index 000000000..cf99c6774 --- /dev/null +++ b/src/Ombi.Helpers.Tests/EmbyHelperTests.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Helpers.Tests +{ + [TestFixture] + public class EmbyHelperTests + { + [TestCaseSource(nameof(UrlData))] + public string TestUrl(string mediaId, string url) + { + return EmbyHelper.GetEmbyMediaUrl(mediaId, url); + } + + public static IEnumerable UrlData + { + get + { + var mediaId = 1; + yield return new TestCaseData(mediaId.ToString(), "http://google.com").Returns($"http://google.com/#!/itemdetails.html?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash"); + yield return new TestCaseData(mediaId.ToString(), "http://google.com/").Returns($"http://google.com/#!/itemdetails.html?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithCustomDomain"); + yield return new TestCaseData(mediaId.ToString(), "https://google.com/").Returns($"https://google.com/#!/itemdetails.html?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithCustomDomain_Https"); + } + } + } +} diff --git a/src/Ombi.Helpers.Tests/HtmlHelperTests.cs b/src/Ombi.Helpers.Tests/HtmlHelperTests.cs new file mode 100644 index 000000000..e03e409b3 --- /dev/null +++ b/src/Ombi.Helpers.Tests/HtmlHelperTests.cs @@ -0,0 +1,41 @@ +using NUnit.Framework; +using System.Collections.Generic; + +namespace Ombi.Helpers.Tests +{ + [TestFixture] + public class HtmlHelperTests + { + [TestCaseSource(nameof(HtmlData))] + public string RemoveHtmlTests(string input) + { + return HtmlHelper.RemoveHtml(input); + } + + public static IEnumerable HtmlData + { + get + { + yield return new TestCaseData("

hi

").Returns("hi").SetName("Simple Html"); + yield return new TestCaseData("

hi

").Returns("hi").SetName("Nested text inside Html"); + yield return new TestCaseData("there is no html here").Returns("there is no html here").SetName("No Html"); + yield return new TestCaseData("there is some html here").Returns("there is some html here").SetName("Html in middle"); + yield return new TestCaseData("there is lots html here").Returns("there is lots html here").SetName("Html in everywhere"); + yield return new TestCaseData("there is some html here").Returns("there is some html here").SetName("Html in with classes"); + yield return new TestCaseData("there is some html here").Returns("there is some html here").SetName("Html in with attribute"); + yield return new TestCaseData("there is some html here").Returns("there is some html here").SetName("Html in with attribute and class"); + } + } + public static IEnumerable OtherData + { + get + { + foreach (var data in HtmlData) + { + yield return data; + } + yield return new TestCaseData("xyz").Returns("xyz").SetName("More Tests"); + } + } + } +} diff --git a/src/Ombi.Helpers.Tests/Ombi.Helpers.Tests.csproj b/src/Ombi.Helpers.Tests/Ombi.Helpers.Tests.csproj index 0517af22d..635186dbf 100644 --- a/src/Ombi.Helpers.Tests/Ombi.Helpers.Tests.csproj +++ b/src/Ombi.Helpers.Tests/Ombi.Helpers.Tests.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Ombi.Helpers.Tests/PagnationHelperTests.cs b/src/Ombi.Helpers.Tests/PagnationHelperTests.cs new file mode 100644 index 000000000..4b058c403 --- /dev/null +++ b/src/Ombi.Helpers.Tests/PagnationHelperTests.cs @@ -0,0 +1,120 @@ +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Ombi.Helpers.Tests +{ + [TestFixture] + public class PaginationHelperTests + { + [TestCaseSource(nameof(TestPageData))] + public void TestPaginationPages(int currentlyLoaded, int toLoad, int maxItemsPerPage, int[] expectedPages) + { + var result = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, maxItemsPerPage); + var pages = result.Select(x => x.Page).ToArray(); + + Assert.That(pages.Length, Is.EqualTo(expectedPages.Length), "Did not contain the correct amount of pages"); + for (var i = 0; i < pages.Length; i++) + { + Assert.That(pages[i], Is.EqualTo(expectedPages[i])); + } + } + + public static IEnumerable TestPageData + { + get + { + yield return new TestCaseData(0, 10, 20, new [] { 1 }).SetName("Pagination_Load_First_Page"); + yield return new TestCaseData(20, 10, 20, new [] { 2 }).SetName("Pagination_Load_Second_Page"); + yield return new TestCaseData(0, 20, 20, new [] { 1 }).SetName("Pagination_Load_Full_First_Page"); + yield return new TestCaseData(20, 20, 20, new [] { 2 }).SetName("Pagination_Load_Full_Second_Page"); + yield return new TestCaseData(10, 20, 20, new [] { 1, 2 }).SetName("Pagination_Load_Half_First_Page_And_Half_Second_Page"); + yield return new TestCaseData(19, 20, 20, new[] { 1, 2 }).SetName("Pagination_Load_End_First_Page_And_Most_Second_Page"); + yield return new TestCaseData(19, 40, 20, new[] { 1, 2, 3 }).SetName("Pagination_Load_End_First_Page_And_Most_Second_And_Third_Page"); + yield return new TestCaseData(10, 10, 20, new[] { 1 }).SetName("Pagination_Load_Half_First_Page"); + yield return new TestCaseData(10, 9, 20, new[] { 1 }).SetName("Pagination_Load_LessThan_Half_First_Page"); + yield return new TestCaseData(20, 10, 20, new[] { 2 }).SetName("Pagination_Load_Half_Second_Page"); + yield return new TestCaseData(20, 9, 20, new[] { 2 }).SetName("Pagination_Load_LessThan_Half_Second_Page"); + yield return new TestCaseData(30, 10, 20, new[] { 2 }).SetName("Pagination_Load_All_Second_Page_With_Half_Take"); + yield return new TestCaseData(49, 1, 50, new[] { 1 }).SetName("Pagination_Load_49_OutOf_50"); + yield return new TestCaseData(49, 1, 100,new[] { 1 }).SetName("Pagination_Load_50_OutOf_100"); + } + } + + [TestCaseSource(nameof(CurrentPositionTestData))] + public void TestCurrentPositionOfPagination(int currentlyLoaded, int toLoad, int maxItemsPerPage, int expectedTake, int expectedSkip) + { + var result = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, maxItemsPerPage); + + var first = result.FirstOrDefault(); + Assert.That(first.Take, Is.EqualTo(expectedTake)); + Assert.That(first.Skip, Is.EqualTo(expectedSkip)); + } + public static IEnumerable CurrentPositionTestData + { + get + { + yield return new TestCaseData(0, 10, 20, 10, 0).SetName("PaginationPosition_Load_First_Half_Of_Page"); + yield return new TestCaseData(10, 10, 20, 10, 10).SetName("PaginationPosition_Load_EndHalf_First_Page"); + yield return new TestCaseData(19, 1, 20, 1, 19).SetName("PaginationPosition_Load_LastItem_Of_First_Page"); + yield return new TestCaseData(20, 20, 300, 20, 20).SetName("PaginationPosition_Load_Full_Second_Page"); + } + } + + [TestCaseSource(nameof(CurrentPositionMultiplePagesTestData))] + public void TestCurrentPositionOfPaginationWithMultiplePages(int currentlyLoaded, int toLoad, int maxItemsPerPage, List data) + { + var result = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, maxItemsPerPage); + + foreach (var r in result) + { + // get result data for this page + var expectedPage = data.FirstOrDefault(x => x.Page == r.Page); + Assert.That(r.Take, Is.EqualTo(expectedPage.ExpectedTake)); + Assert.That(r.Skip, Is.EqualTo(expectedPage.ExpectedSkip)); + } + } + + public static IEnumerable CurrentPositionMultiplePagesTestData + { + get + { + yield return new TestCaseData(10, 20, 20, new List { new MultiplePagesTestData(1, 10, 10), new MultiplePagesTestData(2, 10, 0) }) + .SetName("PaginationPosition_Load_SecondHalf_FirstPage_FirstHalf_SecondPage"); + yield return new TestCaseData(0, 40, 20, new List { new MultiplePagesTestData(1, 20, 0), new MultiplePagesTestData(2, 20, 0) }) + .SetName("PaginationPosition_Load_Full_First_And_SecondPage"); + yield return new TestCaseData(35, 15, 20, new List { new MultiplePagesTestData(2, 5, 15), new MultiplePagesTestData(3, 10, 0) }) + .SetName("PaginationPosition_Load_EndSecondPage_Beginning_ThirdPage"); + yield return new TestCaseData(18, 22, 20, new List { new MultiplePagesTestData(1, 2, 18), new MultiplePagesTestData(2, 20, 0) }) + .SetName("PaginationPosition_Load_EndFirstPage_Full_SecondPage"); + yield return new TestCaseData(38, 4, 20, new List { new MultiplePagesTestData(2, 2, 18), new MultiplePagesTestData(3, 2, 0) }) + .SetName("PaginationPosition_Load_EndSecondPage_Some_ThirdPage"); + yield return new TestCaseData(15, 20, 10, new List { new MultiplePagesTestData(2, 5, 5), new MultiplePagesTestData(3, 10, 0), new MultiplePagesTestData(4, 5, 0) }) + .SetName("PaginationPosition_Load_EndSecondPage_All_ThirdPage_Some_ForthPage"); + yield return new TestCaseData(24, 12, 12, new List { new MultiplePagesTestData(3, 12, 0) }) + .SetName("PaginationPosition_Load_ThirdPage_Of_12"); + yield return new TestCaseData(12, 12, 12, new List { new MultiplePagesTestData(2, 12, 0) }) + .SetName("PaginationPosition_Load_SecondPage_Of_12"); + yield return new TestCaseData(40, 20, 20, new List { new MultiplePagesTestData(3, 20, 0) }) + .SetName("PaginationPosition_Load_FullThird_Page"); + yield return new TestCaseData(240, 12, 20, new List { new MultiplePagesTestData(13, 12, 0) }) + .SetName("PaginationPosition_Load_Page_13"); + } + } + + public class MultiplePagesTestData + { + public MultiplePagesTestData(int page, int take, int skip) + { + Page = page; + ExpectedTake = take; + ExpectedSkip = skip; + } + public int Page { get; set; } + public int ExpectedTake { get; set; } + public int ExpectedSkip { get; set; } + } + + + } +} diff --git a/src/Ombi.Helpers.Tests/UriHelperTests.cs b/src/Ombi.Helpers.Tests/UriHelperTests.cs new file mode 100644 index 000000000..fd21e66f4 --- /dev/null +++ b/src/Ombi.Helpers.Tests/UriHelperTests.cs @@ -0,0 +1,115 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Ombi.Helpers.Tests +{ + [TestFixture] + public class UriHelperTests + { + [TestCaseSource(nameof(UrlData))] + public string ReturnUri(string uri) + { + return UriHelper.ReturnUri(uri).ToString(); + } + public static IEnumerable UrlData + { + get + { + yield return new TestCaseData("https://google.com/").Returns($"https://google.com/").SetName("ReturnUri_With_HttpScheme"); + yield return new TestCaseData("google.com/").Returns($"http://google.com/").SetName("ReturnUri_HttpScheme_Not_Provided"); + yield return new TestCaseData("http://google.com:9090/").Returns($"http://google.com:9090/").SetName("ReturnUri_WithPort"); + yield return new TestCaseData("https://google.com/").Returns($"https://google.com/").SetName("ReturnUri_With_HttpsScheme"); + yield return new TestCaseData("https://hi.google.com/").Returns($"https://hi.google.com/").SetName("ReturnUri_With_SubDomain"); + yield return new TestCaseData("https://google.com/hi").Returns($"https://google.com/hi").SetName("ReturnUri_With_Path"); + yield return new TestCaseData("https://hi.google.com/hi").Returns($"https://hi.google.com/hi").SetName("ReturnUri_With_Path_And_SubDomain"); + } + } + + [TestCaseSource(nameof(UrlWithPortData))] + public string ReturnUriWithPort(string uri, int port) + { + return UriHelper.ReturnUri(uri, port).ToString(); + } + public static IEnumerable UrlWithPortData + { + get + { + + yield return new TestCaseData("https://google.com", 443).Returns($"https://google.com/").SetName("ReturnUri_With_HttpsPort"); + yield return new TestCaseData("https://google.com/", 123).Returns($"https://google.com:123/").SetName("ReturnUri_With_HttpScheme_With_Port"); + yield return new TestCaseData("google.com/", 80).Returns($"http://google.com/").SetName("ReturnUri_HttpScheme_Not_Provided_With_Port"); + yield return new TestCaseData("https://google.com/", 7000).Returns($"https://google.com:7000/").SetName("ReturnUri_With_HttpsScheme_With_Port"); + yield return new TestCaseData("https://hi.google.com/", 1).Returns($"https://hi.google.com:1/").SetName("ReturnUri_With_SubDomain_With_Port"); + yield return new TestCaseData("https://google.com/hi", 443).Returns($"https://google.com/hi").SetName("ReturnUri_With_Path_With_Port"); + yield return new TestCaseData("https://hi.google.com/hi", 443).Returns($"https://hi.google.com/hi").SetName("ReturnUri_With_Path_And_SubDomain_With_Port"); + } + } + + [TestCaseSource(nameof(UrlWithPortWithSSLData))] + public string ReturnUriWithPortAndSSL(string uri, int port, bool ssl) + { + return UriHelper.ReturnUri(uri, port, ssl).ToString(); + } + public static IEnumerable UrlWithPortWithSSLData + { + get + { + foreach (var d in UrlWithPortData) + { + var expected = (string)d.ExpectedResult; + var args = d.Arguments.ToList(); + args.Add(true); + var newExpected = expected.Replace("http://", "https://"); + if (args.Contains(80)) + { + newExpected = expected; + } + d.TestName += "_With_SSL"; + + yield return new TestCaseData(args.ToArray()).Returns(newExpected).SetName(d.TestName); + } + } + } + + [TestCaseSource(nameof(UrlWithPortWithSSLDataCasing))] + public string ReturnUriWithPortAndSSLCasing(string uri, int port, bool ssl) + { + return UriHelper.ReturnUri(uri, port, ssl).ToString(); + } + public static IEnumerable UrlWithPortWithSSLDataCasing + { + get + { + foreach (var d in UrlWithPortData) + { + if (d.TestName.Contains("_Path_")) + { + continue; + } + var expected = (string)d.ExpectedResult; + var args = d.Arguments.ToList(); + for (int i = 0; i < args.Count; i++) + { + if(args[i] is string) + { + args[i] = ((string)args[i]).ToUpper(); + } + } + args.Add(true); + var newExpected = expected.Replace("http://", "https://"); + if (args.Contains(80)) + { + newExpected = expected; + } + d.TestName += "_With_SSL_ToUpper"; + + yield return new TestCaseData(args.ToArray()).Returns(newExpected).SetName(d.TestName); + } + } + } + + } +} diff --git a/src/Ombi.Helpers/CacheService.cs b/src/Ombi.Helpers/CacheService.cs index 4eef62bda..3e98e94ce 100644 --- a/src/Ombi.Helpers/CacheService.cs +++ b/src/Ombi.Helpers/CacheService.cs @@ -28,7 +28,7 @@ namespace Ombi.Helpers return result; } - using (await _mutex.LockAsync()) + //using (await _mutex.LockAsync()) { if (_memoryCache.TryGetValue(cacheKey, out result)) { diff --git a/src/Ombi.Helpers/ClaimConverter.cs b/src/Ombi.Helpers/ClaimConverter.cs deleted file mode 100644 index 09237c8ce..000000000 --- a/src/Ombi.Helpers/ClaimConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Security.Claims; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Ombi.Helpers -{ - public class ClaimConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return (objectType == typeof(System.Security.Claims.Claim)); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var claim = (System.Security.Claims.Claim)value; - JObject jo = new JObject(); - jo.Add("Type", claim.Type); - jo.Add("Value", IsJson(claim.Value) ? new JRaw(claim.Value) : new JValue(claim.Value)); - jo.Add("ValueType", claim.ValueType); - jo.Add("Issuer", claim.Issuer); - jo.Add("OriginalIssuer", claim.OriginalIssuer); - jo.WriteTo(writer); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - JObject jo = JObject.Load(reader); - string type = (string)jo["Type"]; - JToken token = jo["Value"]; - string value = token.Type == JTokenType.String ? (string)token : token.ToString(Formatting.None); - string valueType = (string)jo["ValueType"]; - string issuer = (string)jo["Issuer"]; - string originalIssuer = (string)jo["OriginalIssuer"]; - return new Claim(type, value, valueType, issuer, originalIssuer); - } - - private bool IsJson(string val) - { - return (val != null && - (val.StartsWith("[") && val.EndsWith("]")) || - (val.StartsWith("{") && val.EndsWith("}"))); - } - } -} \ No newline at end of file diff --git a/src/Ombi.Helpers/DemoLists.cs b/src/Ombi.Helpers/DemoLists.cs new file mode 100644 index 000000000..c0d0dd77f --- /dev/null +++ b/src/Ombi.Helpers/DemoLists.cs @@ -0,0 +1,11 @@ +namespace Ombi.Config +{ + public class DemoLists + { + public bool Enabled { get; set; } + public int[] Movies { get; set; } + public int[] TvShows { get; set; } + } + + +} \ No newline at end of file diff --git a/src/Ombi.Helpers/DemoSingleton.cs b/src/Ombi.Helpers/DemoSingleton.cs new file mode 100644 index 000000000..22b6b2f31 --- /dev/null +++ b/src/Ombi.Helpers/DemoSingleton.cs @@ -0,0 +1,13 @@ +namespace Ombi.Helpers +{ + public class DemoSingleton + { + private static DemoSingleton instance; + + private DemoSingleton() { } + + public static DemoSingleton Instance => instance ?? (instance = new DemoSingleton()); + + public bool Demo { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Helpers/EmbyHelper.cs b/src/Ombi.Helpers/EmbyHelper.cs index a9967f21f..b1afe9c99 100644 --- a/src/Ombi.Helpers/EmbyHelper.cs +++ b/src/Ombi.Helpers/EmbyHelper.cs @@ -1,9 +1,4 @@ -using System; -using System.Globalization; -using System.Collections.Generic; -using System.Text; - -namespace Ombi.Helpers +namespace Ombi.Helpers { public class EmbyHelper { @@ -11,6 +6,10 @@ namespace Ombi.Helpers { if (customerServerUrl.HasValue()) { + if(!customerServerUrl.EndsWith("/")) + { + return $"{customerServerUrl}/#!/itemdetails.html?id={mediaId}"; + } return $"{customerServerUrl}#!/itemdetails.html?id={mediaId}"; } else diff --git a/src/Ombi.Helpers/HtmlHelper.cs b/src/Ombi.Helpers/HtmlHelper.cs index 3047fd055..85ca56492 100644 --- a/src/Ombi.Helpers/HtmlHelper.cs +++ b/src/Ombi.Helpers/HtmlHelper.cs @@ -14,6 +14,5 @@ namespace Ombi.Helpers var step2 = Regex.Replace(step1, @"\s{2,}", " "); return step2; } - } } \ No newline at end of file diff --git a/src/Ombi.Helpers/LinqHelpers.cs b/src/Ombi.Helpers/LinqHelpers.cs index 67fdb5c53..af8d44633 100644 --- a/src/Ombi.Helpers/LinqHelpers.cs +++ b/src/Ombi.Helpers/LinqHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Ombi.Helpers { @@ -21,5 +22,31 @@ namespace Ombi.Helpers { return new HashSet(source, comparer); } + + public static IEnumerable Shuffle(this IEnumerable source) + { + return source.Shuffle(new Random()); + } + + public static IEnumerable Shuffle(this IEnumerable source, Random rng) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (rng == null) throw new ArgumentNullException(nameof(rng)); + + return source.ShuffleIterator(rng); + } + + private static IEnumerable ShuffleIterator( + this IEnumerable source, Random rng) + { + var buffer = source.ToList(); + for (int i = 0; i < buffer.Count; i++) + { + int j = rng.Next(i, buffer.Count); + yield return buffer[j]; + + buffer[j] = buffer[i]; + } + } } } \ No newline at end of file diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index 3893dc879..0723800ab 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -32,6 +32,7 @@ namespace Ombi.Helpers public static EventId MattermostNotification => new EventId(4004); public static EventId PushoverNotification => new EventId(4005); public static EventId TelegramNotifcation => new EventId(4006); + public static EventId GotifyNotification => new EventId(4007); public static EventId TvSender => new EventId(5000); public static EventId SonarrSender => new EventId(5001); diff --git a/src/Ombi.Helpers/NotificationAgent.cs b/src/Ombi.Helpers/NotificationAgent.cs index 8990eeba9..18f28105a 100644 --- a/src/Ombi.Helpers/NotificationAgent.cs +++ b/src/Ombi.Helpers/NotificationAgent.cs @@ -10,5 +10,6 @@ Slack = 5, Mattermost = 6, Mobile = 7, + Gotify = 8, } } \ No newline at end of file diff --git a/src/Ombi.Helpers/OmbiRoles.cs b/src/Ombi.Helpers/OmbiRoles.cs index e0cfc5398..02a480fdf 100644 --- a/src/Ombi.Helpers/OmbiRoles.cs +++ b/src/Ombi.Helpers/OmbiRoles.cs @@ -15,5 +15,6 @@ public const string Disabled = nameof(Disabled); public const string ReceivesNewsletter = nameof(ReceivesNewsletter); public const string ManageOwnRequests = nameof(ManageOwnRequests); + public const string EditCustomPage = nameof(EditCustomPage); } } \ No newline at end of file diff --git a/src/Ombi.Helpers/PaginationHelper.cs b/src/Ombi.Helpers/PaginationHelper.cs new file mode 100644 index 000000000..520f56b19 --- /dev/null +++ b/src/Ombi.Helpers/PaginationHelper.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Helpers +{ + public static class PaginationHelper + { + public static List GetNextPages(int currentlyLoaded, int toTake, int maxItemsPerPage) + { + var result = new List(); + + var firstPage = currentlyLoaded / maxItemsPerPage + 1; + var startPos = currentlyLoaded % maxItemsPerPage + 1; + + var lastItemIndex = currentlyLoaded + toTake - 1; + var lastPage = lastItemIndex / maxItemsPerPage + 1; + var stopPos = lastItemIndex % maxItemsPerPage + 1; + + while (currentlyLoaded > maxItemsPerPage) + { + currentlyLoaded -= maxItemsPerPage; + } + if ((currentlyLoaded % maxItemsPerPage) == 0 && (currentlyLoaded % toTake) == 0) + { + currentlyLoaded = 0; + } + + var page1 = new PagesToLoad { Page = firstPage, Skip = currentlyLoaded, Take = toTake }; + + if (toTake + startPos - 1 > maxItemsPerPage) + { + page1.Take = maxItemsPerPage - startPos + 1; + result.Add(page1); + + for (var i = firstPage + 1; i < lastPage; i++) + { + var nextPage = new PagesToLoad { Page = i, Skip = 0, Take = maxItemsPerPage }; + result.Add(nextPage); + } + + var pageN = new PagesToLoad { Page = lastPage, Skip = 0, Take = stopPos }; + result.Add(pageN); + } + else + { + if (page1.Skip + page1.Take > maxItemsPerPage) + { + page1.Skip = 0; + } + result.Add(page1); + } + + return result; + } + } + + public class PagesToLoad + { + public int Page { get; set; } + public int Take { get; set; } + public int Skip { get; set; } + } +} diff --git a/src/Ombi.Helpers/UriHelper.cs b/src/Ombi.Helpers/UriHelper.cs index 6ec8047ae..7b1bde100 100644 --- a/src/Ombi.Helpers/UriHelper.cs +++ b/src/Ombi.Helpers/UriHelper.cs @@ -6,46 +6,44 @@ namespace Ombi.Helpers { private const string Https = "Https"; private const string Http = "Http"; - + public static Uri ReturnUri(this string val) { if (val == null) { throw new ApplicationSettingsException("The URI is null, please check your settings to make sure you have configured the applications correctly."); } - try + var uri = new UriBuilder(); + + if (val.StartsWith("http://", StringComparison.Ordinal)) { - var uri = new UriBuilder(); - - if (val.StartsWith("http://", StringComparison.Ordinal)) - { - uri = new UriBuilder(val); - } - else if (val.StartsWith("https://", StringComparison.Ordinal)) - { - uri = new UriBuilder(val); - } - else if (val.Contains(":")) - { - var split = val.Split(':', '/'); - int port; - int.TryParse(split[1], out port); - - uri = split.Length == 3 - ? new UriBuilder(Http, split[0], port, "/" + split[2]) - : new UriBuilder(Http, split[0], port); - } - else - { - uri = new UriBuilder(Http, val); - } - - return uri.Uri; + uri = new UriBuilder(val); } - catch (Exception exception) + else if (val.StartsWith("https://", StringComparison.Ordinal)) { - throw new Exception(exception.Message, exception); + uri = new UriBuilder(val); } + else if (val.Contains(":")) + { + var split = val.Split(':', '/'); + int port; + int.TryParse(split[1], out port); + + uri = split.Length == 3 + ? new UriBuilder(Http, split[0], port, "/" + split[2]) + : new UriBuilder(Http, split[0], port); + } + else + { + if(val.EndsWith("/")) + { + // Remove a trailing slash, since the URIBuilder adds one + val = val.Remove(val.Length - 1, 1); + } + uri = new UriBuilder(Http, val); + } + + return uri.Uri; } /// @@ -64,37 +62,40 @@ namespace Ombi.Helpers { throw new ApplicationSettingsException("The URI is null, please check your settings to make sure you have configured the applications correctly."); } - try - { - var uri = new UriBuilder(); + var uri = new UriBuilder(); - if (val.StartsWith("http://", StringComparison.Ordinal)) - { - var split = val.Split('/'); - uri = split.Length >= 4 ? new UriBuilder(Http, split[2], port, "/" + split[3]) : new UriBuilder(new Uri($"{val}:{port}")); - } - else if (val.StartsWith("https://", StringComparison.Ordinal)) - { - var split = val.Split('/'); - uri = split.Length >= 4 - ? new UriBuilder(Https, split[2], port, "/" + split[3]) - : new UriBuilder(Https, split[2], port); - } - else if (ssl) - { - uri = new UriBuilder(Https, val, port); - } - else - { - uri = new UriBuilder(Http, val, port); - } - - return uri.Uri; - } - catch (Exception exception) + if (val.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) { - throw new Exception(exception.Message, exception); + var split = val.Split('/'); + uri = split.Length >= 4 ? new UriBuilder(Http, split[2], port, "/" + split[3]) : new UriBuilder(new Uri($"{val}:{port}")); } + else if (val.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) + { + var split = val.Split('/'); + uri = split.Length >= 4 + ? new UriBuilder(Https, split[2], port, "/" + split[3]) + : new UriBuilder(Https, split[2], port); + } + else if ((ssl || port == 443) && port != 80) + { + if (val.EndsWith("/")) + { + // Remove a trailing slash, since the URIBuilder adds one + val = val.Remove(val.Length - 1, 1); + } + uri = new UriBuilder(Https, val, port); + } + else + { + if (val.EndsWith("/")) + { + // Remove a trailing slash, since the URIBuilder adds one + val = val.Remove(val.Length - 1, 1); + } + uri = new UriBuilder(Http, val, port); + } + + return uri.Uri; } public static Uri ReturnUriWithSubDir(this string val, int port, bool ssl, string subDir) @@ -112,7 +113,7 @@ namespace Ombi.Helpers return uriBuilder.Uri; } - + } public class ApplicationSettingsException : Exception diff --git a/src/Ombi.Mapping/AutoMapperProfile.cs b/src/Ombi.Mapping/AutoMapperProfile.cs index b0b1fd725..a39bc8e71 100644 --- a/src/Ombi.Mapping/AutoMapperProfile.cs +++ b/src/Ombi.Mapping/AutoMapperProfile.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using AutoMapper; using AutoMapper.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -12,9 +11,9 @@ namespace Ombi.Mapping { public static IServiceCollection AddOmbiMappingProfile(this IServiceCollection services) { - System.Reflection.Assembly ass = typeof(AutoMapperProfile).GetTypeInfo().Assembly; + Assembly ass = typeof(AutoMapperProfile).GetTypeInfo().Assembly; var assemblies = new List(); - foreach (System.Reflection.TypeInfo ti in ass.DefinedTypes) + foreach (TypeInfo ti in ass.DefinedTypes) { if (ti.ImplementedInterfaces.Contains(typeof(IProfileConfiguration))) { diff --git a/src/Ombi.Mapping/Profiles/MovieProfile.cs b/src/Ombi.Mapping/Profiles/MovieProfile.cs index aae7fae64..7d981a7bc 100644 --- a/src/Ombi.Mapping/Profiles/MovieProfile.cs +++ b/src/Ombi.Mapping/Profiles/MovieProfile.cs @@ -4,6 +4,8 @@ using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Models.Search; using Ombi.Core.Models.Search.V2; using Ombi.TheMovieDbApi.Models; +using Keywords = Ombi.Core.Models.Search.V2.Keywords; +using KeywordsValue = Ombi.Api.TheMovieDb.Models.KeywordsValue; namespace Ombi.Mapping.Profiles { @@ -86,6 +88,22 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap() + .ForMember(x => x.Name, o => o.MapFrom(s => s.name)) + .ForMember(x => x.Overview, o => o.MapFrom(s => s.overview)) + .ForMember(x => x.Collection, o => o.MapFrom(s => s.parts)); + + CreateMap() + .ForMember(x => x.Id, o => o.MapFrom(s => s.id)) + .ForMember(x => x.Overview, o => o.MapFrom(s => s.overview)) + .ForMember(x => x.PosterPath, o => o.MapFrom(s => s.poster_path)) + .ForMember(x => x.Title, o => o.MapFrom(s => s.title)); + + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index 139290f2b..f460ce78b 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -19,6 +19,7 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/TvProfileV2.cs b/src/Ombi.Mapping/Profiles/TvProfileV2.cs new file mode 100644 index 000000000..3a8aeeb62 --- /dev/null +++ b/src/Ombi.Mapping/Profiles/TvProfileV2.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using AutoMapper; +using Ombi.Api.TvMaze.Models.V2; +using Ombi.Core.Models.Search; +using Ombi.Core.Models.Search.V2; +using Ombi.Helpers; + +namespace Ombi.Mapping.Profiles +{ + public class TvProfileV2 : Profile + { + public TvProfileV2() + { + CreateMap() + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.externals.thetvdb)) + .ForMember(dest => dest.FirstAired, opts => opts.MapFrom(src => src.premiered)) + .ForMember(dest => dest.ImdbId, opts => opts.MapFrom(src => src.externals.imdb)) + .ForMember(dest => dest.Network, opts => opts.MapFrom(src => src.network.name)) + .ForMember(dest => dest.NetworkId, opts => opts.MapFrom(src => src.network.id.ToString())) + .ForMember(dest => dest.Overview, opts => opts.MapFrom(src => src.summary.RemoveHtml())) + .ForMember(dest => dest.Rating, + opts => opts.MapFrom(src => src.rating.average.ToString(CultureInfo.CurrentUICulture))) + .ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.runtime.ToString())) + .ForMember(dest => dest.SeriesId, opts => opts.MapFrom(src => src.id)) + .ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.name)) + .ForMember(dest => dest.Network, opts => opts.MapFrom(src => src.network)) + .ForMember(dest => dest.Images, opts => opts.MapFrom(src => src.image)) + .ForMember(dest => dest.Cast, opts => opts.MapFrom(src => src._embedded.cast)) + .ForMember(dest => dest.Crew, opts => opts.MapFrom(src => src._embedded.crew)) + .ForMember(dest => dest.Banner, + opts => opts.MapFrom(src => + !string.IsNullOrEmpty(src.image.medium) + ? src.image.medium.Replace("http", "https") + : string.Empty)) + .ForMember(dest => dest.Status, opts => opts.MapFrom(src => src.status)); + + CreateMap() + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.id)) + .ForMember(dest => dest.Country, opts => opts.MapFrom(src => src.country)) + .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.name)); + + CreateMap() + .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.name)) + .ForMember(dest => dest.Code, opts => opts.MapFrom(src => src.code)) + .ForMember(dest => dest.Timezone, opts => opts.MapFrom(src => src.timezone)); + + CreateMap() + .ForMember(dest => dest.Medium, opts => opts.MapFrom(src => src.medium)) + .ForMember(dest => dest.Original, opts => opts.MapFrom(src => src.original)); + + CreateMap() + .ForMember(dest => dest.Character, opts => opts.MapFrom(src => src.character)) + .ForMember(dest => dest.Person, opts => opts.MapFrom(src => src.person)) + .ForMember(dest => dest.Voice, opts => opts.MapFrom(src => src.voice)) + .ForMember(dest => dest.Self, opts => opts.MapFrom(src => src.self)); + + CreateMap() + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.id)) + .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.name)) + .ForMember(dest => dest.Image, opts => opts.MapFrom(src => src.image)) + .ForMember(dest => dest.Url, opts => opts.MapFrom(src => src.url)); + + CreateMap() + .ForMember(dest => dest.Person, opts => opts.MapFrom(src => src.person)) + .ForMember(dest => dest.Type, opts => opts.MapFrom(src => src.type)); + + CreateMap() + .ForMember(dest => dest.Person, opts => opts.MapFrom(src => src.person)) + .ForMember(dest => dest.Self, opts => opts.MapFrom(src => src.self)) + .ForMember(dest => dest.Voice, opts => opts.MapFrom(src => src.voice)) + .ForMember(dest => dest.Character, opts => opts.MapFrom(src => src.character)); + + CreateMap() + .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.name)) + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.id)) + .ForMember(dest => dest.Url, opts => opts.MapFrom(src => src.url)) + .ForMember(dest => dest.Image, opts => opts.MapFrom(src => src.image)); + + CreateMap().ReverseMap(); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs b/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs index b29122be0..fc80de193 100644 --- a/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs +++ b/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs @@ -13,7 +13,7 @@ namespace Ombi.Notifications.Templates if (string.IsNullOrEmpty(_templateLocation)) { #if DEBUG - _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", + _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.2", "Templates", "BasicTemplate.html"); #else _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html"); diff --git a/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj b/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj index 5436dff22..d5db2cf26 100644 --- a/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj +++ b/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj @@ -5,10 +5,10 @@ - + - - + + diff --git a/src/Ombi.Notifications/Agents/GotifyNotification.cs b/src/Ombi.Notifications/Agents/GotifyNotification.cs new file mode 100644 index 000000000..e1c9fc1db --- /dev/null +++ b/src/Ombi.Notifications/Agents/GotifyNotification.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Gotify; +using Ombi.Core.Settings; +using Ombi.Helpers; +using Ombi.Notifications.Models; +using Ombi.Settings.Settings.Models; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Notifications.Agents +{ + public class GotifyNotification : BaseNotification, IGotifyNotification + { + public GotifyNotification(IGotifyApi api, ISettingsService sn, ILogger log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, + ISettingsService s, IRepository sub, IMusicRequestRepository music, + IRepository userPref) : base(sn, r, m, t, s, log, sub, music, userPref) + { + Api = api; + Logger = log; + } + + public override string NotificationName => "GotifyNotification"; + + private IGotifyApi Api { get; } + private ILogger Logger { get; } + + protected override bool ValidateConfiguration(GotifySettings settings) + { + return settings.Enabled && !string.IsNullOrEmpty(settings.BaseUrl) && !string.IsNullOrEmpty(settings.ApplicationToken); + } + + protected override async Task NewRequest(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.NewRequest); + } + + + protected override async Task NewIssue(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.Issue); + } + + protected override async Task IssueComment(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.IssueComment); + } + + protected override async Task IssueResolved(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.IssueResolved); + } + + protected override async Task AddedToRequestQueue(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.ItemAddedToFaultQueue); + } + + protected override async Task RequestDeclined(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.RequestDeclined); + } + + protected override async Task RequestApproved(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.RequestApproved); + } + + protected override async Task AvailableRequest(NotificationOptions model, GotifySettings settings) + { + await Run(model, settings, NotificationType.RequestAvailable); + } + + protected override async Task Send(NotificationMessage model, GotifySettings settings) + { + try + { + await Api.PushAsync(settings.BaseUrl, settings.ApplicationToken, model.Subject, model.Message, settings.Priority); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.GotifyNotification, e, "Failed to send Gotify notification"); + } + } + + protected override async Task Test(NotificationOptions model, GotifySettings settings) + { + var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + private async Task Run(NotificationOptions model, GotifySettings settings, NotificationType type) + { + var parsed = await LoadTemplate(NotificationAgent.Gotify, type, model); + if (parsed.Disabled) + { + Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Gotify}"); + return; + } + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + + await Send(notification, settings); + } + } +} diff --git a/src/Ombi.Notifications/Agents/Interfaces/IGotifyNotification.cs b/src/Ombi.Notifications/Agents/Interfaces/IGotifyNotification.cs new file mode 100644 index 000000000..a85421938 --- /dev/null +++ b/src/Ombi.Notifications/Agents/Interfaces/IGotifyNotification.cs @@ -0,0 +1,6 @@ +namespace Ombi.Notifications.Agents +{ + public interface IGotifyNotification : INotification + { + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/MattermostNotification.cs b/src/Ombi.Notifications/Agents/MattermostNotification.cs index f64a0fe21..ed3084c19 100644 --- a/src/Ombi.Notifications/Agents/MattermostNotification.cs +++ b/src/Ombi.Notifications/Agents/MattermostNotification.cs @@ -52,7 +52,7 @@ namespace Ombi.Notifications.Agents private void AddOtherInformation(NotificationOptions model, NotificationMessage notification, NotificationMessageContent parsed) { - notification.Other.Add("image", parsed.Image); + notification.Other.Add("image", parsed?.Image ?? string.Empty); notification.Other.Add("title", model.RequestType == RequestType.Movie ? MovieRequest.Title : TvRequest.Title); } diff --git a/src/Ombi.Notifications/BaseNotification.cs b/src/Ombi.Notifications/BaseNotification.cs index 57eafedd9..001f68f45 100644 --- a/src/Ombi.Notifications/BaseNotification.cs +++ b/src/Ombi.Notifications/BaseNotification.cs @@ -26,8 +26,6 @@ namespace Ombi.Notifications MovieRepository = movie; TvRepository = tv; CustomizationSettings = customization; - Settings.ClearCache(); - CustomizationSettings.ClearCache(); RequestSubscription = sub; _log = log; AlbumRepository = album; @@ -55,14 +53,12 @@ namespace Ombi.Notifications public async Task NotifyAsync(NotificationOptions model) { - Settings.ClearCache(); var configuration = await GetConfiguration(); await NotifyAsync(model, configuration); } public async Task NotifyAsync(NotificationOptions model, Settings.Settings.Models.Settings settings) { - Settings.ClearCache(); if (settings == null) await NotifyAsync(model); var notificationSettings = (T)settings; diff --git a/src/Ombi.Notifications/GenericEmailProvider.cs b/src/Ombi.Notifications/GenericEmailProvider.cs index d3f207234..15f17af92 100644 --- a/src/Ombi.Notifications/GenericEmailProvider.cs +++ b/src/Ombi.Notifications/GenericEmailProvider.cs @@ -4,7 +4,9 @@ using EnsureThat; using MailKit.Net.Smtp; using Microsoft.Extensions.Logging; using MimeKit; +using MimeKit.Utils; using Ombi.Core.Settings; +using Ombi.Helpers; using Ombi.Notifications.Models; using Ombi.Notifications.Templates; using Ombi.Settings.Settings.Models; @@ -36,6 +38,15 @@ namespace Ombi.Notifications var customization = await CustomizationSettings.GetSettingsAsync(); var html = email.LoadTemplate(model.Subject, model.Message, null, customization.Logo); + + var messageId = MimeUtils.GenerateMessageId(); + if (customization.ApplicationUrl.HasValue()) + { + if (Uri.TryCreate(customization.ApplicationUrl, UriKind.RelativeOrAbsolute, out var url)) + { + messageId = MimeUtils.GenerateMessageId(url.IdnHost); + } + } var textBody = string.Empty; @@ -49,7 +60,8 @@ namespace Ombi.Notifications var message = new MimeMessage { Body = body.ToMessageBody(), - Subject = model.Subject + Subject = model.Subject, + MessageId = messageId }; message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress)); message.To.Add(new MailboxAddress(model.To, model.To)); diff --git a/src/Ombi.Notifications/NotificationMessageCurlys.cs b/src/Ombi.Notifications/NotificationMessageCurlys.cs index cedda3735..bfa8213bc 100644 --- a/src/Ombi.Notifications/NotificationMessageCurlys.cs +++ b/src/Ombi.Notifications/NotificationMessageCurlys.cs @@ -17,7 +17,7 @@ namespace Ombi.Notifications public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s, UserNotificationPreferences pref) { LoadIssues(opts); - + RequestId = req.Id.ToString(); string title; if (req == null) { @@ -68,6 +68,7 @@ namespace Ombi.Notifications { LoadIssues(opts); + RequestId = req.Id.ToString(); string title; if (req == null) { @@ -114,6 +115,7 @@ namespace Ombi.Notifications public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref) { LoadIssues(opts); + RequestId = req.Id.ToString(); string title; if (req == null) { @@ -239,6 +241,7 @@ namespace Ombi.Notifications public string UserPreference { get; set; } public string DenyReason { get; set; } public string AvailableDate { get; set; } + public string RequestId { get; set; } // System Defined private string LongDate => DateTime.Now.ToString("D"); @@ -275,6 +278,7 @@ namespace Ombi.Notifications {nameof(UserPreference),UserPreference}, {nameof(DenyReason),DenyReason}, {nameof(AvailableDate),AvailableDate}, + {nameof(RequestId),RequestId}, }; } } \ No newline at end of file diff --git a/src/Ombi.Notifications/Ombi.Notifications.csproj b/src/Ombi.Notifications/Ombi.Notifications.csproj index 2b5c95154..dfc939776 100644 --- a/src/Ombi.Notifications/Ombi.Notifications.csproj +++ b/src/Ombi.Notifications/Ombi.Notifications.csproj @@ -10,11 +10,12 @@ - + + diff --git a/src/Ombi.Schedule.Tests/IssuesPurgeTests.cs b/src/Ombi.Schedule.Tests/IssuesPurgeTests.cs index 932022cd8..1ddafb55b 100644 --- a/src/Ombi.Schedule.Tests/IssuesPurgeTests.cs +++ b/src/Ombi.Schedule.Tests/IssuesPurgeTests.cs @@ -44,12 +44,12 @@ namespace Ombi.Schedule.Tests new Issues { Status = IssueStatus.Resolved, - ResovledDate = DateTime.Now.AddDays(-5).AddHours(-1) + ResovledDate = DateTime.UtcNow.AddDays(-5).AddHours(-8) } }; Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 }); - Repo.Setup(x => x.GetAll()).Returns(new EnumerableQuery(issues)); + Repo.Setup(x => x.GetAll()).Returns(issues.AsQueryable()); await Job.Start(); Assert.That(issues.First().Status, Is.EqualTo(IssueStatus.Deleted)); @@ -57,7 +57,7 @@ namespace Ombi.Schedule.Tests } [Test] - public async Task DoesNot_Delete_AnyIssues() + public async Task DoesNot_Delete_AllIssues() { var issues = new List() { @@ -81,5 +81,31 @@ namespace Ombi.Schedule.Tests Assert.That(issues[1].Status, Is.EqualTo(IssueStatus.Deleted)); Repo.Verify(x => x.SaveChangesAsync(), Times.Once); } + + [Test] + public async Task DoesNot_Delete_AnyIssues() + { + var issues = new List() + { + new Issues + { + Status = IssueStatus.Resolved, + ResovledDate = DateTime.Now.AddDays(-2) + }, + new Issues + { + Status = IssueStatus.Resolved, + ResovledDate = DateTime.Now.AddDays(-4) + } + }; + + Settings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new IssueSettings { DeleteIssues = true, DaysAfterResolvedToDelete = 5 }); + Repo.Setup(x => x.GetAll()).Returns(new EnumerableQuery(issues)); + await Job.Start(); + + Assert.That(issues[0].Status, Is.Not.EqualTo(IssueStatus.Deleted)); + Assert.That(issues[1].Status, Is.Not.EqualTo(IssueStatus.Deleted)); + Repo.Verify(x => x.SaveChangesAsync(), Times.Once); + } } } \ No newline at end of file diff --git a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj index 0c83dd755..367221735 100644 --- a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj +++ b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj @@ -6,11 +6,12 @@ + - + - - + + diff --git a/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs b/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs index 55c9dc288..cd212b1e5 100644 --- a/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs +++ b/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Castle.Components.DictionaryAdapter; using Hangfire; using Moq; +using MockQueryable.Moq; using NUnit.Framework; using Ombi.Core.Notifications; using Ombi.Schedule.Jobs.Plex; @@ -68,7 +69,6 @@ namespace Ombi.Schedule.Tests } [Test] - [Ignore("EF IAsyncQueryProvider")] public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex() { var request = new ChildRequests @@ -90,21 +90,25 @@ namespace Ombi.Schedule.Tests } } } + }, + RequestedUser = new OmbiUser + { + Email = "abc" } }; - _tv.Setup(x => x.GetChild()).Returns(new List { request }.AsQueryable()); + _tv.Setup(x => x.GetChild()).Returns(new List { request }.AsQueryable().BuildMock().Object); _repo.Setup(x => x.GetAllEpisodes()).Returns(new List { new PlexEpisode { Series = new PlexServerContent { - ImdbId = 1.ToString(), + TvDbId = 1.ToString(), }, EpisodeNumber = 1, SeasonNumber = 2 } - }.AsQueryable); + }.AsQueryable().BuildMock().Object); _repo.Setup(x => x.Include(It.IsAny>(),It.IsAny>>())); await Checker.Start(); diff --git a/src/Ombi.Schedule/JobSetup.cs b/src/Ombi.Schedule/JobSetup.cs index 5cc818441..38686396f 100644 --- a/src/Ombi.Schedule/JobSetup.cs +++ b/src/Ombi.Schedule/JobSetup.cs @@ -81,7 +81,6 @@ namespace Ombi.Schedule RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s)); - RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s)); RecurringJob.AddOrUpdate(() => _resender.Start(), JobSettingsHelper.ResendFailedRequests(s)); RecurringJob.AddOrUpdate(() => _mediaDatabaseRefresh.Start(), JobSettingsHelper.MediaDatabaseRefresh(s)); } diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 4972f940f..e22c0ca51 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -28,7 +28,6 @@ namespace Ombi.Schedule.Jobs.Emby _repo = repo; _episodeSync = epSync; _metadata = metadata; - _settings.ClearCache(); } private readonly ILogger _logger; diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs index 2bb417d3d..962b08cda 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs @@ -49,7 +49,6 @@ namespace Ombi.Schedule.Jobs.Emby _settings = s; _repo = repo; _avaliabilityChecker = checker; - _settings.ClearCache(); } private readonly ISettingsService _settings; diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs index 2dadd4bd4..280a61ab4 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs @@ -50,8 +50,6 @@ namespace Ombi.Schedule.Jobs.Emby _log = log; _embySettings = embySettings; _userManagementSettings = ums; - _userManagementSettings.ClearCache(); - _embySettings.ClearCache(); } private readonly IEmbyApi _api; diff --git a/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs b/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs index d19fe561a..2a50b5b38 100644 --- a/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs +++ b/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs @@ -1,19 +1,16 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using Hangfire; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; using Ombi.Api.Lidarr; -using Ombi.Api.Radarr; using Ombi.Core.Settings; using Ombi.Helpers; using Ombi.Settings.Settings.Models.External; using Ombi.Store.Context; using Ombi.Store.Entities; -using Serilog; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Ombi.Schedule.Jobs.Lidarr @@ -29,7 +26,6 @@ namespace Ombi.Schedule.Jobs.Lidarr _ctx = ctx; _job = job; _availability = availability; - _lidarrSettings.ClearCache(); } private readonly ISettingsService _lidarrSettings; diff --git a/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs b/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs index 4117ee44a..e9a64f2a3 100644 --- a/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs +++ b/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs @@ -1,19 +1,16 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using Hangfire; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; using Ombi.Api.Lidarr; -using Ombi.Api.Radarr; using Ombi.Core.Settings; using Ombi.Helpers; using Ombi.Settings.Settings.Models.External; using Ombi.Store.Context; using Ombi.Store.Entities; -using Serilog; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Ombi.Schedule.Jobs.Lidarr @@ -29,7 +26,6 @@ namespace Ombi.Schedule.Jobs.Lidarr _ctx = ctx; _job = background; _albumSync = album; - _lidarrSettings.ClearCache(); } private readonly ISettingsService _lidarrSettings; diff --git a/src/Ombi.Schedule/Jobs/Ombi/Interfaces/IssuesPurge.cs b/src/Ombi.Schedule/Jobs/Ombi/Interfaces/IssuesPurge.cs index 92ca31071..b25bc49ed 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/Interfaces/IssuesPurge.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/Interfaces/IssuesPurge.cs @@ -28,9 +28,10 @@ namespace Ombi.Schedule.Jobs.Ombi return; } - var now = DateTime.Now.AddDays(-settings.DaysAfterResolvedToDelete).Date; + var today = DateTime.UtcNow.Date; + var resolved = _issuesRepository.GetAll().Where(x => x.Status == IssueStatus.Resolved); - var toDelete = resolved.Where(x => x.ResovledDate.HasValue && x.ResovledDate.Value.Date <= now); + var toDelete = resolved.Where(x => x.ResovledDate.HasValue && (today - x.ResovledDate.Value.Date).TotalDays >= settings.DaysAfterResolvedToDelete); foreach (var d in toDelete) { diff --git a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index baa7fdcae..ed0bf227f 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -10,28 +10,23 @@ using Ombi.Schedule.Jobs.Emby; using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Store.Repository; -namespace Ombi.Schedule.Jobs.Plex +namespace Ombi.Schedule.Jobs.Ombi { public class MediaDatabaseRefresh : IMediaDatabaseRefresh { - public MediaDatabaseRefresh(ISettingsService s, ILogger log, IPlexApi plexApi, - IPlexContentRepository plexRepo, IPlexContentSync c, IEmbyContentRepository embyRepo, IEmbyContentSync embySync) + public MediaDatabaseRefresh(ISettingsService s, ILogger log, + IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IEmbyContentSync embySync) { _settings = s; _log = log; - _api = plexApi; _plexRepo = plexRepo; - _plexContentSync = c; _embyRepo = embyRepo; _embyContentSync = embySync; - _settings.ClearCache(); } private readonly ISettingsService _settings; private readonly ILogger _log; - private readonly IPlexApi _api; private readonly IPlexContentRepository _plexRepo; - private readonly IPlexContentSync _plexContentSync; private readonly IEmbyContentRepository _embyRepo; private readonly IEmbyContentSync _embyContentSync; diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index ba4bd5c62..6c59f4c0f 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -16,6 +16,7 @@ using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TvMaze; using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Notifications; using Ombi.Notifications.Models; @@ -36,7 +37,7 @@ namespace Ombi.Schedule.Jobs.Ombi ISettingsService emailSettings, INotificationTemplatesRepository templateRepo, UserManager um, ISettingsService newsletter, ILogger log, ILidarrApi lidarrApi, IRepository albumCache, ISettingsService lidarrSettings, - ISettingsService ombiSettings) + ISettingsService ombiSettings, ISettingsService plexSettings, ISettingsService embySettings) { _plex = plex; _emby = emby; @@ -49,16 +50,13 @@ namespace Ombi.Schedule.Jobs.Ombi _emailSettings = emailSettings; _newsletterSettings = newsletter; _userManager = um; - _emailSettings.ClearCache(); - _customizationSettings.ClearCache(); - _newsletterSettings.ClearCache(); _log = log; _lidarrApi = lidarrApi; _lidarrAlbumRepository = albumCache; _lidarrSettings = lidarrSettings; _ombiSettings = ombiSettings; - _ombiSettings.ClearCache(); - _lidarrSettings.ClearCache(); + _plexSettings = plexSettings; + _embySettings = embySettings; } private readonly IPlexContentRepository _plex; @@ -77,6 +75,8 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly ILidarrApi _lidarrApi; private readonly IRepository _lidarrAlbumRepository; private readonly ISettingsService _lidarrSettings; + private readonly ISettingsService _plexSettings; + private readonly ISettingsService _embySettings; public async Task Start(NewsletterSettings settings, bool test) { @@ -132,6 +132,8 @@ namespace Ombi.Schedule.Jobs.Ombi _log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count()); _log.LogInformation("Emby Episodes to send: {0}", embyEpisodesToSend.Count()); + var plexSettings = await _plexSettings.GetSettingsAsync(); + var embySettings = await _embySettings.GetSettingsAsync(); var body = string.Empty; if (test) { @@ -140,11 +142,11 @@ namespace Ombi.Schedule.Jobs.Ombi var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10).ToHashSet(); var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet(); var lidarr = lidarrContent.OrderByDescending(x => x.AddedAt).Take(10).ToHashSet(); - body = await BuildHtml(plexm, embym, plext, embyt, lidarr, settings); + body = await BuildHtml(plexm, embym, plext, embyt, lidarr, settings, embySettings, plexSettings); } else { - body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings); + body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings, embySettings, plexSettings); if (body.IsNullOrEmpty()) { return; @@ -333,7 +335,8 @@ namespace Ombi.Schedule.Jobs.Ombi } private async Task BuildHtml(IQueryable plexContentToSend, IQueryable embyContentToSend, - HashSet plexEpisodes, HashSet embyEp, HashSet albums, NewsletterSettings settings) + HashSet plexEpisodes, HashSet embyEp, HashSet albums, NewsletterSettings settings, EmbySettings embySettings, + PlexSettings plexSettings) { var ombiSettings = await _ombiSettings.GetSettingsAsync(); var sb = new StringBuilder(); @@ -349,8 +352,16 @@ namespace Ombi.Schedule.Jobs.Ombi sb.Append(""); sb.Append(""); sb.Append(""); - await ProcessPlexMovies(plexMovies, sb, ombiSettings.DefaultLanguageCode); - await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode); + if (plexSettings.Enable) + { + await ProcessPlexMovies(plexMovies, sb, ombiSettings.DefaultLanguageCode); + } + + if (embySettings.Enable) + { + await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode); + } + sb.Append(""); sb.Append("
"); sb.Append(""); @@ -367,8 +378,16 @@ namespace Ombi.Schedule.Jobs.Ombi sb.Append(""); sb.Append(""); sb.Append(""); - await ProcessPlexTv(plexEpisodes, sb); - await ProcessEmbyTv(embyEp, sb); + if (plexSettings.Enable) + { + await ProcessPlexTv(plexEpisodes, sb); + } + + if (embySettings.Enable) + { + await ProcessEmbyTv(embyEp, sb); + } + sb.Append(""); sb.Append("
"); sb.Append(""); diff --git a/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs b/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs index 5ec8978b6..783fe5f9d 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs @@ -5,18 +5,13 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Net; -using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Hangfire; -using Hangfire.Console; using Hangfire.Server; using Microsoft.Extensions.Logging; - -using Ombi.Api.Service; -using Ombi.Api.Service.Models; using Ombi.Core.Processor; using Ombi.Core.Settings; using Ombi.Helpers; @@ -40,7 +35,6 @@ namespace Ombi.Schedule.Jobs.Ombi Settings = s; _processProvider = proc; _appConfig = appConfig; - Settings.ClearCache(); } private ILogger Logger { get; } diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index d58c29ddc..c9ba5c6b3 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Hangfire; using Microsoft.Extensions.Logging; +using Ombi.Api.Emby; using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TvMaze; @@ -21,7 +22,8 @@ namespace Ombi.Schedule.Jobs.Ombi { public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, ILogger log, ITvMazeApi tvApi, ISettingsService plexSettings, - IMovieDbApi movieApi, ISettingsService embySettings, IPlexAvailabilityChecker plexAvailability, IEmbyAvaliabilityChecker embyAvaliability) + IMovieDbApi movieApi, ISettingsService embySettings, IPlexAvailabilityChecker plexAvailability, IEmbyAvaliabilityChecker embyAvaliability, + IEmbyApi embyApi) { _plexRepo = plexRepo; _embyRepo = embyRepo; @@ -32,6 +34,7 @@ namespace Ombi.Schedule.Jobs.Ombi _embySettings = embySettings; _plexAvailabilityChecker = plexAvailability; _embyAvaliabilityChecker = embyAvaliability; + _embyApi = embyApi; } private readonly IPlexContentRepository _plexRepo; @@ -43,6 +46,7 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly ITvMazeApi _tvApi; private readonly ISettingsService _plexSettings; private readonly ISettingsService _embySettings; + private readonly IEmbyApi _embyApi; public async Task Start() { @@ -54,11 +58,11 @@ namespace Ombi.Schedule.Jobs.Ombi { await StartPlex(); } - + var embySettings = await _embySettings.GetSettingsAsync(); if (embySettings.Enable) { - await StartEmby(); + await StartEmby(embySettings); } } catch (Exception e) @@ -123,9 +127,9 @@ namespace Ombi.Schedule.Jobs.Ombi await StartPlexTv(allTv); } - private async Task StartEmby() + private async Task StartEmby(EmbySettings s) { - await StartEmbyMovies(); + await StartEmbyMovies(s); await StartEmbyTv(); } @@ -158,7 +162,7 @@ namespace Ombi.Schedule.Jobs.Ombi _plexRepo.UpdateWithoutSave(show); } tvCount++; - if (tvCount >= 20) + if (tvCount >= 75) { await _plexRepo.SaveChangesAsync(); tvCount = 0; @@ -198,7 +202,7 @@ namespace Ombi.Schedule.Jobs.Ombi _embyRepo.UpdateWithoutSave(show); } tvCount++; - if (tvCount >= 20) + if (tvCount >= 75) { await _embyRepo.SaveChangesAsync(); tvCount = 0; @@ -229,7 +233,7 @@ namespace Ombi.Schedule.Jobs.Ombi _plexRepo.UpdateWithoutSave(movie); } movieCount++; - if (movieCount >= 20) + if (movieCount >= 75) { await _plexRepo.SaveChangesAsync(); movieCount = 0; @@ -239,31 +243,56 @@ namespace Ombi.Schedule.Jobs.Ombi await _plexRepo.SaveChangesAsync(); } - private async Task StartEmbyMovies() + private async Task StartEmbyMovies(EmbySettings settings) { var allMovies = _embyRepo.GetAll().Where(x => x.Type == EmbyMediaType.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())); int movieCount = 0; foreach (var movie in allMovies) { - var hasImdb = movie.ImdbId.HasValue(); - var hasTheMovieDb = movie.TheMovieDbId.HasValue(); + movie.ImdbId.HasValue(); + movie.TheMovieDbId.HasValue(); // Movies don't really use TheTvDb - if (!hasImdb) + // Check if it even has 1 ID + if (!movie.HasImdb && !movie.HasTheMovieDb) { - var imdbId = await GetImdbId(hasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty); + // Ok this sucks, + // The only think I can think that has happened is that we scanned Emby before Emby has got the metadata + // So let's recheck emby to see if they have got the metadata now + _log.LogInformation($"Movie {movie.Title} does not have a ImdbId or TheMovieDbId, so rechecking emby"); + foreach (var server in settings.Servers) + { + _log.LogInformation($"Checking server {server.Name} for upto date metadata"); + var movieInfo = await _embyApi.GetMovieInformation(movie.EmbyId, server.ApiKey, server.AdministratorId, + server.FullUri); + + if (movieInfo.ProviderIds?.Imdb.HasValue() ?? false) + { + movie.ImdbId = movieInfo.ProviderIds.Imdb; + } + + if (movieInfo.ProviderIds?.Tmdb.HasValue() ?? false) + { + movie.TheMovieDbId = movieInfo.ProviderIds.Tmdb; + } + } + } + + if (!movie.HasImdb) + { + var imdbId = await GetImdbId(movie.HasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty); movie.ImdbId = imdbId; _embyRepo.UpdateWithoutSave(movie); } - if (!hasTheMovieDb) + if (!movie.HasTheMovieDb) { - var id = await GetTheMovieDbId(false, hasImdb, string.Empty, movie.ImdbId, movie.Title, true); + var id = await GetTheMovieDbId(false, movie.HasImdb, string.Empty, movie.ImdbId, movie.Title, true); movie.TheMovieDbId = id; _embyRepo.UpdateWithoutSave(movie); } movieCount++; - if (movieCount >= 20) + if (movieCount >= 75) { await _embyRepo.SaveChangesAsync(); movieCount = 0; diff --git a/src/Ombi.Schedule/Jobs/Ombi/WelcomeEmail.cs b/src/Ombi.Schedule/Jobs/Ombi/WelcomeEmail.cs index e260ebed3..f98072e9e 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/WelcomeEmail.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/WelcomeEmail.cs @@ -20,8 +20,6 @@ namespace Ombi.Schedule.Jobs.Ombi _email = provider; _templates = template; _customizationSettings = c; - email.ClearCache(); - _customizationSettings.ClearCache(); } private readonly ISettingsService _emailSettings; diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs index c39b80c1f..964832b44 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs @@ -57,7 +57,6 @@ namespace Ombi.Schedule.Jobs.Plex EpisodeSync = epsiodeSync; Metadata = metadataRefresh; Checker = checker; - plex.ClearCache(); } private ISettingsService Plex { get; } diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs index 5652d126b..6ab5a5941 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs @@ -26,7 +26,6 @@ namespace Ombi.Schedule.Jobs.Plex _api = plexApi; _repo = repo; _availabilityChecker = a; - _settings.ClearCache(); } private readonly ISettingsService _settings; diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs b/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs index 4f6bf2550..105e8876d 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs @@ -24,8 +24,6 @@ namespace Ombi.Schedule.Jobs.Plex _log = log; _plexSettings = plexSettings; _userManagementSettings = ums; - _userManagementSettings.ClearCache(); - _plexSettings.ClearCache(); } private readonly IPlexApi _api; diff --git a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs index 5c954def8..f61747ac6 100644 --- a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs @@ -22,7 +22,6 @@ namespace Ombi.Schedule.Jobs.Radarr RadarrApi = radarrApi; Logger = log; _ctx = ctx; - RadarrSettings.ClearCache(); } private ISettingsService RadarrSettings { get; } diff --git a/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs b/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs index 92e0c2d55..2c8d03b1d 100644 --- a/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs +++ b/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs @@ -22,7 +22,6 @@ namespace Ombi.Schedule.Jobs.SickRage _api = api; _log = l; _ctx = ctx; - _settings.ClearCache(); } private readonly ISettingsService _settings; diff --git a/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs b/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs index e4c00c726..c77e23394 100644 --- a/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs @@ -25,7 +25,6 @@ namespace Ombi.Schedule.Jobs.Sonarr _api = api; _log = l; _ctx = ctx; - _settings.ClearCache(); } private readonly ISettingsService _settings; diff --git a/src/Ombi.Schedule/Ombi.Schedule.csproj b/src/Ombi.Schedule/Ombi.Schedule.csproj index ff3a17115..827db0d9a 100644 --- a/src/Ombi.Schedule/Ombi.Schedule.csproj +++ b/src/Ombi.Schedule/Ombi.Schedule.csproj @@ -10,13 +10,13 @@ - - - + + + - + diff --git a/src/Ombi.Settings.Tests/CustomizationSettingsTests.cs b/src/Ombi.Settings.Tests/CustomizationSettingsTests.cs new file mode 100644 index 000000000..6d83b99d9 --- /dev/null +++ b/src/Ombi.Settings.Tests/CustomizationSettingsTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using Ombi.Settings.Settings.Models; +using System.Collections.Generic; + +namespace Tests +{ + [TestFixture] + public class CustomizationSettingsTests + { + + [TestCaseSource(nameof(TestData))] + public string AddToUrlTests(string applicationUrl, string append) + { + var c = new CustomizationSettings + { + ApplicationUrl = applicationUrl + }; + c.AddToUrl(append); + + return c.ApplicationUrl; + } + + public static IEnumerable TestData + { + get + { + yield return new TestCaseData("https://google.com/", "token?").Returns("https://google.com/token?").SetName("ForwardSlash_On_AppUrl_NotOn_Append"); + } + } + } +} \ No newline at end of file diff --git a/src/Ombi.Settings.Tests/Ombi.Settings.Tests.csproj b/src/Ombi.Settings.Tests/Ombi.Settings.Tests.csproj new file mode 100644 index 000000000..db214be34 --- /dev/null +++ b/src/Ombi.Settings.Tests/Ombi.Settings.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.2 + + false + + + + + + + + + + + + + diff --git a/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs b/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs index 5f0287fc4..a9fcf8f83 100644 --- a/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs +++ b/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs @@ -21,7 +21,7 @@ if (ApplicationUrl.EndsWith("/")) { - ApplicationUrl.Remove(ApplicationUrl.Length - 1); + ApplicationUrl = ApplicationUrl.Remove(ApplicationUrl.Length - 1); } if (!part.StartsWith("/")) { diff --git a/src/Ombi.Settings/Settings/Models/Notifications/GotifySettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/GotifySettings.cs new file mode 100644 index 000000000..f0325b0f2 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/GotifySettings.cs @@ -0,0 +1,10 @@ +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class GotifySettings : Settings + { + public bool Enabled { get; set; } + public string BaseUrl { get; set; } + public string ApplicationToken { get; set; } + public sbyte Priority { get; set; } = 4; + } +} \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/SettingsService.cs b/src/Ombi.Settings/Settings/SettingsService.cs index 3162c34c2..57ae482a2 100644 --- a/src/Ombi.Settings/Settings/SettingsService.cs +++ b/src/Ombi.Settings/Settings/SettingsService.cs @@ -61,7 +61,7 @@ namespace Ombi.Settings.Settings var model = obj; return model; - }, DateTime.Now.AddHours(2)); + }, DateTime.Now.AddHours(5)); } public bool SaveSettings(T model) diff --git a/src/Ombi.Store/Context/ExternalContext.cs b/src/Ombi.Store/Context/ExternalContext.cs index eb2be6450..ff0581091 100644 --- a/src/Ombi.Store/Context/ExternalContext.cs +++ b/src/Ombi.Store/Context/ExternalContext.cs @@ -13,6 +13,7 @@ namespace Ombi.Store.Context if (_created) return; _created = true; + Database.SetCommandTimeout(60); Database.Migrate(); } diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index bb84f1272..98f4cd9c3 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -17,7 +17,9 @@ namespace Ombi.Store.Context { if (_created) return; + _created = true; + Database.SetCommandTimeout(60); Database.Migrate(); } @@ -87,43 +89,6 @@ namespace Ombi.Store.Context public void Seed() { - // VACUUM; - Database.ExecuteSqlCommand("VACUUM;"); - - // Make sure we have the roles - var newsletterRole = Roles.Where(x => x.Name == OmbiRoles.ReceivesNewsletter); - if (!newsletterRole.Any()) - { - Roles.Add(new IdentityRole(OmbiRoles.ReceivesNewsletter) - { - NormalizedName = OmbiRoles.ReceivesNewsletter.ToUpper() - }); - SaveChanges(); - } - var requestMusicRole = Roles.Where(x => x.Name == OmbiRoles.RequestMusic); - if (!requestMusicRole.Any()) - { - Roles.Add(new IdentityRole(OmbiRoles.RequestMusic) - { - NormalizedName = OmbiRoles.RequestMusic.ToUpper() - }); - Roles.Add(new IdentityRole(OmbiRoles.AutoApproveMusic) - { - NormalizedName = OmbiRoles.AutoApproveMusic.ToUpper() - }); - SaveChanges(); - } - - var manageOwnRequestsRole = Roles.Where(x => x.Name == OmbiRoles.ManageOwnRequests); - if (!manageOwnRequestsRole.Any()) - { - Roles.Add(new IdentityRole(OmbiRoles.ManageOwnRequests) - { - NormalizedName = OmbiRoles.ManageOwnRequests.ToUpper() - }); - SaveChanges(); - } - // Make sure we have the API User var apiUserExists = Users.Any(x => x.UserName.Equals("Api", StringComparison.CurrentCultureIgnoreCase)); if (!apiUserExists) @@ -144,6 +109,7 @@ namespace Ombi.Store.Context var allAgents = Enum.GetValues(typeof(NotificationAgent)).Cast().ToList(); var allTypes = Enum.GetValues(typeof(NotificationType)).Cast().ToList(); + var needToSave = false; foreach (var agent in allAgents) { foreach (var notificationType in allTypes) @@ -153,6 +119,8 @@ namespace Ombi.Store.Context // We already have this continue; } + + needToSave = true; NotificationTemplates notificationToAdd; switch (notificationType) { @@ -267,7 +235,11 @@ namespace Ombi.Store.Context NotificationTemplates.Add(notificationToAdd); } } - SaveChanges(); + + if (needToSave) + { + SaveChanges(); + } } } } \ No newline at end of file diff --git a/src/Ombi.Store/Context/SettingsContext.cs b/src/Ombi.Store/Context/SettingsContext.cs index 6a53e598f..926f8cdba 100644 --- a/src/Ombi.Store/Context/SettingsContext.cs +++ b/src/Ombi.Store/Context/SettingsContext.cs @@ -14,6 +14,7 @@ namespace Ombi.Store.Context if (_created) return; _created = true; + Database.SetCommandTimeout(60); Database.Migrate(); } @@ -27,7 +28,7 @@ namespace Ombi.Store.Context { i.StoragePath = string.Empty; } - optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath, "OmbiSettings.db")}"); + optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath, "OmbiSettings" + ".db")}"); } public void Seed() @@ -63,13 +64,6 @@ namespace Ombi.Store.Context }); SaveChanges(); } - - SaveChanges(); - } - - ~SettingsContext() - { - } } } \ No newline at end of file diff --git a/src/Ombi.Store/Entities/Requests/MovieRequests.cs b/src/Ombi.Store/Entities/Requests/MovieRequests.cs index 677a4292c..0ccf55f0b 100644 --- a/src/Ombi.Store/Entities/Requests/MovieRequests.cs +++ b/src/Ombi.Store/Entities/Requests/MovieRequests.cs @@ -8,6 +8,10 @@ namespace Ombi.Store.Entities.Requests [Table("MovieRequests")] public class MovieRequests : FullBaseRequest { + public MovieRequests() + { + RequestType = RequestType.Movie; + } public int TheMovieDbId { get; set; } public int? IssueId { get; set; } [ForeignKey(nameof(IssueId))] diff --git a/src/Ombi.Store/Entities/Requests/SeasonRequests.cs b/src/Ombi.Store/Entities/Requests/SeasonRequests.cs index 521cf5b94..ea9f74547 100644 --- a/src/Ombi.Store/Entities/Requests/SeasonRequests.cs +++ b/src/Ombi.Store/Entities/Requests/SeasonRequests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; @@ -15,6 +16,7 @@ namespace Ombi.Store.Repository.Requests public int ChildRequestId { get; set; } [ForeignKey(nameof(ChildRequestId))] public ChildRequests ChildRequest { get; set; } + [NotMapped] public bool SeasonAvailable { get; set; } } public class EpisodeRequests : Entity @@ -27,9 +29,10 @@ namespace Ombi.Store.Repository.Requests public bool Approved { get; set; } public bool Requested { get; set; } - public int SeasonId { get; set; } [ForeignKey(nameof(SeasonId))] public SeasonRequests Season { get; set; } + + [NotMapped] public string AirDateDisplay => AirDate == DateTime.MinValue ? "Unknown" : AirDate.ToString(CultureInfo.InvariantCulture); } } \ No newline at end of file diff --git a/src/Ombi.Store/Entities/Requests/TvRequests.cs b/src/Ombi.Store/Entities/Requests/TvRequests.cs index 432bc88ab..5e33c016f 100644 --- a/src/Ombi.Store/Entities/Requests/TvRequests.cs +++ b/src/Ombi.Store/Entities/Requests/TvRequests.cs @@ -17,10 +17,6 @@ namespace Ombi.Store.Entities.Requests public DateTime ReleaseDate { get; set; } public string Status { get; set; } - /// - /// This is so we can correctly send the right amount of seasons to Sonarr - /// - [NotMapped] public int TotalSeasons { get; set; } public List ChildRequests { get; set; } diff --git a/src/Ombi.Store/Migrations/20190216224539_Roles.Designer.cs b/src/Ombi.Store/Migrations/20190216224539_Roles.Designer.cs new file mode 100644 index 000000000..f0aeb88bc --- /dev/null +++ b/src/Ombi.Store/Migrations/20190216224539_Roles.Designer.cs @@ -0,0 +1,1212 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context; + +namespace Ombi.Store.Migrations +{ + [DbContext(typeof(OmbiContext))] + [Migration("20190216224539_Roles")] + partial class Roles + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.1-servicing-10028"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Type"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("ApplicationConfiguration"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuditArea"); + + b.Property("AuditType"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("User"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId") + .IsRequired(); + + b.Property("ImdbId"); + + b.Property("ProviderId"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId"); + + b.Property("EpisodeNumber"); + + b.Property("ImdbId"); + + b.Property("ParentId"); + + b.Property("ProviderId"); + + b.Property("SeasonNumber"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.Property("SettingsName"); + + b.HasKey("Id"); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ArtistId"); + + b.Property("ForeignAlbumId"); + + b.Property("Monitored"); + + b.Property("PercentOfTracks"); + + b.Property("ReleaseDate"); + + b.Property("Title"); + + b.Property("TrackCount"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtistId"); + + b.Property("ArtistName"); + + b.Property("ForeignArtistId"); + + b.Property("Monitored"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("Message"); + + b.Property("NotificationType"); + + b.Property("Subject"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("PlayerId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("Alias"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("EmbyConnectUserId"); + + b.Property("EpisodeRequestLimit"); + + b.Property("LastLoggedIn"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("MovieRequestLimit"); + + b.Property("MusicRequestLimit"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("ProviderUserId"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserAccessToken"); + + b.Property("UserName") + .HasMaxLength(256); + + b.Property("UserType"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ParentKey"); + + b.Property("PlexContentId"); + + b.Property("PlexServerContentId"); + + b.Property("SeasonKey"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ImdbId"); + + b.Property("Key"); + + b.Property("Quality"); + + b.Property("ReleaseYear"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("HasFile"); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("AlbumId"); + + b.Property("ContentId"); + + b.Property("ContentType"); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestQueue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Completed"); + + b.Property("Dts"); + + b.Property("Error"); + + b.Property("RequestId"); + + b.Property("RetryCount"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RequestQueue"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestSubscription"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("ArtistName"); + + b.Property("Available"); + + b.Property("Cover"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("Disk"); + + b.Property("ForeignAlbumId"); + + b.Property("ForeignArtistId"); + + b.Property("MarkedAsApproved"); + + b.Property("MarkedAsAvailable"); + + b.Property("MarkedAsDenied"); + + b.Property("Rating"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedByAlias"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("AlbumRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("IssueId"); + + b.Property("MarkedAsApproved"); + + b.Property("MarkedAsAvailable"); + + b.Property("MarkedAsDenied"); + + b.Property("ParentRequestId"); + + b.Property("RequestType"); + + b.Property("RequestedByAlias"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("SeriesType"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Comment"); + + b.Property("Date"); + + b.Property("IssuesId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueCategoryId"); + + b.Property("IssueId"); + + b.Property("ProviderId"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("ResovledDate"); + + b.Property("Status"); + + b.Property("Subject"); + + b.Property("Title"); + + b.Property("UserReportedId"); + + b.HasKey("Id"); + + b.HasIndex("IssueCategoryId"); + + b.HasIndex("IssueId"); + + b.HasIndex("UserReportedId"); + + b.ToTable("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Background"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("DigitalReleaseDate"); + + b.Property("ImdbId"); + + b.Property("IssueId"); + + b.Property("LangCode"); + + b.Property("MarkedAsApproved"); + + b.Property("MarkedAsAvailable"); + + b.Property("MarkedAsDenied"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedByAlias"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("RootPathOverride"); + + b.Property("Status"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeCount"); + + b.Property("RequestDate"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Background"); + + b.Property("ImdbId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RootFolder"); + + b.Property("Status"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("HasFile"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Token"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("UserId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RadarrQualityProfile"); + + b.Property("RadarrRootPath"); + + b.Property("SonarrQualityProfile"); + + b.Property("SonarrQualityProfileAnime"); + + b.Property("SonarrRootPath"); + + b.Property("SonarrRootPathAnime"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserQualityProfiles"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Date"); + + b.Property("Deleted"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.Property("VoteType"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Votes"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AirDate"); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("EpisodeNumber"); + + b.Property("Requested"); + + b.Property("SeasonId"); + + b.Property("Title"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChildRequestId"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("NotificationUserIds") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent") + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest") + .WithMany("ChildRequests") + .HasForeignKey("ParentRequestId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues") + .WithMany("Comments") + .HasForeignKey("IssuesId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory") + .WithMany() + .HasForeignKey("IssueCategoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") + .WithMany() + .HasForeignKey("UserReportedId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("UserNotificationPreferences") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest") + .WithMany("SeasonRequests") + .HasForeignKey("ChildRequestId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/20190216224539_Roles.cs b/src/Ombi.Store/Migrations/20190216224539_Roles.cs new file mode 100644 index 000000000..9c2f91c12 --- /dev/null +++ b/src/Ombi.Store/Migrations/20190216224539_Roles.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Ombi.Helpers; + +namespace Ombi.Store.Migrations +{ + public partial class Roles : Migration + { + protected override void Up(MigrationBuilder mb) + { + // Make sure we have the roles + InsertRole(mb, OmbiRoles.ReceivesNewsletter); + InsertRole(mb, OmbiRoles.RequestMusic); + InsertRole(mb, OmbiRoles.AutoApproveMusic); + InsertRole(mb, OmbiRoles.ManageOwnRequests); + InsertRole(mb, OmbiRoles.EditCustomPage); + } + + private void InsertRole(MigrationBuilder mb, string role) + { + mb.Sql($@" +INSERT INTO AspnetRoles(Id, ConcurrencyStamp, Name, NormalizedName) +SELECT '{Guid.NewGuid().ToString()}','{Guid.NewGuid().ToString()}','{role}', '{role.ToUpper()}' +WHERE NOT EXISTS(SELECT 1 FROM AspnetRoles WHERE Name = '{role}');"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Ombi.Store/Migrations/20190216231519_TvRequestsTotalSeasons.Designer.cs b/src/Ombi.Store/Migrations/20190216231519_TvRequestsTotalSeasons.Designer.cs new file mode 100644 index 000000000..7341ff5fe --- /dev/null +++ b/src/Ombi.Store/Migrations/20190216231519_TvRequestsTotalSeasons.Designer.cs @@ -0,0 +1,1214 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context; + +namespace Ombi.Store.Migrations +{ + [DbContext(typeof(OmbiContext))] + [Migration("20190216231519_TvRequestsTotalSeasons")] + partial class TvRequestsTotalSeasons + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.1-servicing-10028"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Type"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("ApplicationConfiguration"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuditArea"); + + b.Property("AuditType"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("User"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId") + .IsRequired(); + + b.Property("ImdbId"); + + b.Property("ProviderId"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId"); + + b.Property("EpisodeNumber"); + + b.Property("ImdbId"); + + b.Property("ParentId"); + + b.Property("ProviderId"); + + b.Property("SeasonNumber"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.Property("SettingsName"); + + b.HasKey("Id"); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ArtistId"); + + b.Property("ForeignAlbumId"); + + b.Property("Monitored"); + + b.Property("PercentOfTracks"); + + b.Property("ReleaseDate"); + + b.Property("Title"); + + b.Property("TrackCount"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtistId"); + + b.Property("ArtistName"); + + b.Property("ForeignArtistId"); + + b.Property("Monitored"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("Message"); + + b.Property("NotificationType"); + + b.Property("Subject"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("PlayerId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("Alias"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("EmbyConnectUserId"); + + b.Property("EpisodeRequestLimit"); + + b.Property("LastLoggedIn"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("MovieRequestLimit"); + + b.Property("MusicRequestLimit"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("ProviderUserId"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserAccessToken"); + + b.Property("UserName") + .HasMaxLength(256); + + b.Property("UserType"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ParentKey"); + + b.Property("PlexContentId"); + + b.Property("PlexServerContentId"); + + b.Property("SeasonKey"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ImdbId"); + + b.Property("Key"); + + b.Property("Quality"); + + b.Property("ReleaseYear"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("HasFile"); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("AlbumId"); + + b.Property("ContentId"); + + b.Property("ContentType"); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestQueue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Completed"); + + b.Property("Dts"); + + b.Property("Error"); + + b.Property("RequestId"); + + b.Property("RetryCount"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RequestQueue"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestSubscription"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("ArtistName"); + + b.Property("Available"); + + b.Property("Cover"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("Disk"); + + b.Property("ForeignAlbumId"); + + b.Property("ForeignArtistId"); + + b.Property("MarkedAsApproved"); + + b.Property("MarkedAsAvailable"); + + b.Property("MarkedAsDenied"); + + b.Property("Rating"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedByAlias"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("AlbumRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("IssueId"); + + b.Property("MarkedAsApproved"); + + b.Property("MarkedAsAvailable"); + + b.Property("MarkedAsDenied"); + + b.Property("ParentRequestId"); + + b.Property("RequestType"); + + b.Property("RequestedByAlias"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("SeriesType"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Comment"); + + b.Property("Date"); + + b.Property("IssuesId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueCategoryId"); + + b.Property("IssueId"); + + b.Property("ProviderId"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("ResovledDate"); + + b.Property("Status"); + + b.Property("Subject"); + + b.Property("Title"); + + b.Property("UserReportedId"); + + b.HasKey("Id"); + + b.HasIndex("IssueCategoryId"); + + b.HasIndex("IssueId"); + + b.HasIndex("UserReportedId"); + + b.ToTable("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Background"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("DigitalReleaseDate"); + + b.Property("ImdbId"); + + b.Property("IssueId"); + + b.Property("LangCode"); + + b.Property("MarkedAsApproved"); + + b.Property("MarkedAsAvailable"); + + b.Property("MarkedAsDenied"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedByAlias"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("RootPathOverride"); + + b.Property("Status"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeCount"); + + b.Property("RequestDate"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Background"); + + b.Property("ImdbId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RootFolder"); + + b.Property("Status"); + + b.Property("Title"); + + b.Property("TotalSeasons"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("HasFile"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Token"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("UserId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RadarrQualityProfile"); + + b.Property("RadarrRootPath"); + + b.Property("SonarrQualityProfile"); + + b.Property("SonarrQualityProfileAnime"); + + b.Property("SonarrRootPath"); + + b.Property("SonarrRootPathAnime"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserQualityProfiles"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Date"); + + b.Property("Deleted"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.Property("VoteType"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Votes"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AirDate"); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("EpisodeNumber"); + + b.Property("Requested"); + + b.Property("SeasonId"); + + b.Property("Title"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChildRequestId"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("NotificationUserIds") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent") + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest") + .WithMany("ChildRequests") + .HasForeignKey("ParentRequestId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues") + .WithMany("Comments") + .HasForeignKey("IssuesId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory") + .WithMany() + .HasForeignKey("IssueCategoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") + .WithMany() + .HasForeignKey("UserReportedId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("UserNotificationPreferences") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest") + .WithMany("SeasonRequests") + .HasForeignKey("ChildRequestId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/20190216231519_TvRequestsTotalSeasons.cs b/src/Ombi.Store/Migrations/20190216231519_TvRequestsTotalSeasons.cs new file mode 100644 index 000000000..0bae50c15 --- /dev/null +++ b/src/Ombi.Store/Migrations/20190216231519_TvRequestsTotalSeasons.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ombi.Store.Migrations +{ + public partial class TvRequestsTotalSeasons : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TotalSeasons", + table: "TvRequests", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TotalSeasons", + table: "TvRequests"); + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs index 50a34a931..466011d43 100644 --- a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs @@ -819,6 +819,8 @@ namespace Ombi.Store.Migrations b.Property("Title"); + b.Property("TotalSeasons"); + b.Property("TvDbId"); b.HasKey("Id"); diff --git a/src/Ombi.Store/Ombi.Store.csproj b/src/Ombi.Store/Ombi.Store.csproj index f905c9ffe..497b1ec2d 100644 --- a/src/Ombi.Store/Ombi.Store.csproj +++ b/src/Ombi.Store/Ombi.Store.csproj @@ -12,14 +12,12 @@ - - + + + - - - diff --git a/src/Ombi.Store/Repository/BaseRepository.cs b/src/Ombi.Store/Repository/BaseRepository.cs index 1679035dd..0741a79b6 100644 --- a/src/Ombi.Store/Repository/BaseRepository.cs +++ b/src/Ombi.Store/Repository/BaseRepository.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; +using Ombi.Helpers; using Ombi.Store.Context; using Ombi.Store.Entities; @@ -30,7 +31,7 @@ namespace Ombi.Store.Repository return _db.AsQueryable(); } - public async Task FirstOrDefaultAsync(Expression> predicate) + public async Task FirstOrDefaultAsync(Expression> predicate) { return await _db.FirstOrDefaultAsync(predicate); } @@ -40,32 +41,32 @@ namespace Ombi.Store.Repository _db.AddRange(content); if (save) { - await _ctx.SaveChangesAsync(); + await InternalSaveChanges(); } } public async Task Add(T content) { await _db.AddAsync(content); - await _ctx.SaveChangesAsync(); + await InternalSaveChanges(); return content; } public async Task Delete(T request) { _db.Remove(request); - await _ctx.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task DeleteRange(IEnumerable req) { _db.RemoveRange(req); - await _ctx.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task SaveChangesAsync() { - return await _ctx.SaveChangesAsync(); + return await InternalSaveChanges(); } public IIncludableQueryable Include( @@ -80,6 +81,11 @@ namespace Ombi.Store.Repository await _ctx.Database.ExecuteSqlCommandAsync(sql); } + protected async Task InternalSaveChanges() + { + return await _ctx.SaveChangesAsync(); + } + private bool _disposed; // Protected implementation of Dispose pattern. @@ -92,7 +98,7 @@ namespace Ombi.Store.Repository { _ctx?.Dispose(); } - + _disposed = true; } diff --git a/src/Ombi.Store/Repository/EmbyContentRepository.cs b/src/Ombi.Store/Repository/EmbyContentRepository.cs index 4d32e8da2..2ada709ab 100644 --- a/src/Ombi.Store/Repository/EmbyContentRepository.cs +++ b/src/Ombi.Store/Repository/EmbyContentRepository.cs @@ -72,7 +72,7 @@ namespace Ombi.Store.Repository public async Task Update(EmbyContent existingContent) { Db.EmbyContent.Update(existingContent); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public IQueryable GetAllEpisodes() @@ -83,7 +83,7 @@ namespace Ombi.Store.Repository public async Task Add(EmbyEpisode content) { await Db.EmbyEpisode.AddAsync(content); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); return content; } public async Task GetEpisodeByEmbyId(string key) @@ -94,12 +94,13 @@ namespace Ombi.Store.Repository public async Task AddRange(IEnumerable content) { Db.EmbyEpisode.AddRange(content); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public void UpdateWithoutSave(EmbyContent existingContent) { Db.EmbyContent.Update(existingContent); } + } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs index 175d0e6a9..8f3968dc0 100644 --- a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs @@ -45,7 +45,7 @@ namespace Ombi.Store.Repository Db.Attach(template); Db.Entry(template).State = EntityState.Modified; } - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task UpdateRange(IEnumerable templates) @@ -56,16 +56,21 @@ namespace Ombi.Store.Repository Db.Attach(t); Db.Entry(t).State = EntityState.Modified; } - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task Insert(NotificationTemplates entity) { var settings = await Db.NotificationTemplates.AddAsync(entity).ConfigureAwait(false); - await Db.SaveChangesAsync().ConfigureAwait(false); + await InternalSaveChanges().ConfigureAwait(false); return settings.Entity; } + private async Task InternalSaveChanges() + { + return await Db.SaveChangesAsync(); + } + private bool _disposed; // Protected implementation of Dispose pattern. protected virtual void Dispose(bool disposing) diff --git a/src/Ombi.Store/Repository/PlexContentRepository.cs b/src/Ombi.Store/Repository/PlexContentRepository.cs index 2c9c28d09..37275a47c 100644 --- a/src/Ombi.Store/Repository/PlexContentRepository.cs +++ b/src/Ombi.Store/Repository/PlexContentRepository.cs @@ -96,7 +96,7 @@ namespace Ombi.Store.Repository public async Task Update(PlexServerContent existingContent) { Db.PlexServerContent.Update(existingContent); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public void UpdateWithoutSave(PlexServerContent existingContent) { @@ -106,7 +106,7 @@ namespace Ombi.Store.Repository public async Task UpdateRange(IEnumerable existingContent) { Db.PlexServerContent.UpdateRange(existingContent); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public IQueryable GetAllEpisodes() @@ -127,14 +127,14 @@ namespace Ombi.Store.Repository public async Task Add(PlexEpisode content) { await Db.PlexEpisode.AddAsync(content); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); return content; } public async Task DeleteEpisode(PlexEpisode content) { Db.PlexEpisode.Remove(content); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task GetEpisodeByKey(int key) @@ -144,7 +144,7 @@ namespace Ombi.Store.Repository public async Task AddRange(IEnumerable content) { Db.PlexEpisode.AddRange(content); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs index d4a550528..2cea81200 100644 --- a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Ombi.Helpers; using Ombi.Store.Context; using Ombi.Store.Entities.Requests; @@ -70,12 +71,12 @@ namespace Ombi.Store.Repository.Requests Db.MovieRequests.Attach(request); Db.Update(request); } - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task Save() { - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/MusicRequestRepository.cs b/src/Ombi.Store/Repository/Requests/MusicRequestRepository.cs index 59edf265a..971d53b39 100644 --- a/src/Ombi.Store/Repository/Requests/MusicRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/MusicRequestRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Ombi.Helpers; using Ombi.Store.Context; using Ombi.Store.Entities.Requests; @@ -61,12 +62,12 @@ namespace Ombi.Store.Repository.Requests Db.AlbumRequests.Attach(request); Db.Update(request); } - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task Save() { - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs index daac7d4df..91e885b37 100644 --- a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Ombi.Helpers; using Ombi.Store.Context; using Ombi.Store.Entities.Requests; @@ -101,20 +102,20 @@ namespace Ombi.Store.Repository.Requests public async Task Save() { - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task Add(TvRequests request) { await Db.TvRequests.AddAsync(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); return request; } public async Task AddChild(ChildRequests request) { await Db.ChildRequests.AddAsync(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); return request; } @@ -122,33 +123,38 @@ namespace Ombi.Store.Repository.Requests public async Task Delete(TvRequests request) { Db.TvRequests.Remove(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task DeleteChild(ChildRequests request) { Db.ChildRequests.Remove(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task DeleteChildRange(IEnumerable request) { Db.ChildRequests.RemoveRange(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task Update(TvRequests request) { Db.Update(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task UpdateChild(ChildRequests request) { Db.Update(request); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); + } + + private async Task InternalSaveChanges() + { + return await Db.SaveChangesAsync(); } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/SettingsJsonRepository.cs b/src/Ombi.Store/Repository/SettingsJsonRepository.cs index 66cf57b18..909a68480 100644 --- a/src/Ombi.Store/Repository/SettingsJsonRepository.cs +++ b/src/Ombi.Store/Repository/SettingsJsonRepository.cs @@ -62,14 +62,14 @@ namespace Ombi.Store.Repository { //_cache.Remove(GetName(entity.SettingsName)); Db.Settings.Remove(entity); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public async Task UpdateAsync(GlobalSettings entity) { //_cache.Remove(GetName(entity.SettingsName)); Db.Update(entity); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public void Delete(GlobalSettings entity) @@ -91,6 +91,11 @@ namespace Ombi.Store.Repository return $"{entity}Json"; } + private async Task InternalSaveChanges() + { + return await Db.SaveChangesAsync(); + } + private bool _disposed; protected virtual void Dispose(bool disposing) { diff --git a/src/Ombi.Store/Repository/TokenRepository.cs b/src/Ombi.Store/Repository/TokenRepository.cs index d766c5690..d0a501bd5 100644 --- a/src/Ombi.Store/Repository/TokenRepository.cs +++ b/src/Ombi.Store/Repository/TokenRepository.cs @@ -4,6 +4,7 @@ using Ombi.Store.Entities; using System; using System.Linq; using System.Threading.Tasks; +using Ombi.Helpers; namespace Ombi.Store.Repository { @@ -19,12 +20,16 @@ namespace Ombi.Store.Repository public async Task CreateToken(Tokens token) { await Db.Tokens.AddAsync(token); - await Db.SaveChangesAsync(); + await InternalSaveChanges(); } public IQueryable GetToken(string tokenId) { return Db.Tokens.Where(x => x.Token == tokenId); } + private async Task InternalSaveChanges() + { + return await Db.SaveChangesAsync(); + } } } diff --git a/src/Ombi.Test.Common/MockHelper.cs b/src/Ombi.Test.Common/MockHelper.cs new file mode 100644 index 000000000..3e871c48f --- /dev/null +++ b/src/Ombi.Test.Common/MockHelper.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Identity; +using MockQueryable.Moq; +using Moq; +using Ombi.Core.Authentication; +using Ombi.Store.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ombi.Test.Common +{ + public static class MockHelper + { + public static Mock MockUserManager(List ls) + { + var store = new Mock>(); + //var u = new OmbiUserManager(store.Object, null, null, null, null, null, null, null, null,null,null,null,null) + var mgr = new Mock(store.Object, null, null, null, null, null, null, null, null, null, null, null, null); + mgr.Object.UserValidators.Add(new UserValidator()); + mgr.Object.PasswordValidators.Add(new PasswordValidator()); + + var userMock = ls.AsQueryable().BuildMock(); + + mgr.Setup(x => x.Users).Returns(userMock.Object); + mgr.Setup(x => x.DeleteAsync(It.IsAny())).ReturnsAsync(IdentityResult.Success); + mgr.Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())).ReturnsAsync(IdentityResult.Success).Callback((x, y) => ls.Add(x)); + mgr.Setup(x => x.UpdateAsync(It.IsAny())).ReturnsAsync(IdentityResult.Success); + + return mgr; + } + } +} diff --git a/src/Ombi.Test.Common/Ombi.Test.Common.csproj b/src/Ombi.Test.Common/Ombi.Test.Common.csproj new file mode 100644 index 000000000..cd028e602 --- /dev/null +++ b/src/Ombi.Test.Common/Ombi.Test.Common.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/src/Ombi.Tests/Ombi.Tests.csproj b/src/Ombi.Tests/Ombi.Tests.csproj index fdfec1cb2..4eb65deeb 100644 --- a/src/Ombi.Tests/Ombi.Tests.csproj +++ b/src/Ombi.Tests/Ombi.Tests.csproj @@ -9,10 +9,10 @@ - + - - + + diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 379636929..c04d4c58a 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -9,12 +9,12 @@ namespace Ombi.Api.TheMovieDb { Task GetMovieInformation(int movieId); Task GetMovieInformationWithExtraInfo(int movieId, string langCode = "en"); - Task> NowPlaying(string languageCode); - Task> PopularMovies(string languageCode); + Task> NowPlaying(string languageCode, int? page = null); + Task> PopularMovies(string languageCode, int? page = null); Task> SearchMovie(string searchTerm, int? year, string languageCode); Task> SearchTv(string searchTerm); - Task> TopRated(string languageCode); - Task> Upcoming(string languageCode); + Task> TopRated(string languageCode, int? page = null); + Task> Upcoming(string languageCode, int? page = null); Task> SimilarMovies(int movieId, string langCode); Task Find(string externalId, ExternalSource source); Task GetTvExternals(int theMovieDbId); @@ -23,5 +23,7 @@ namespace Ombi.Api.TheMovieDb Task GetActorMovieCredits(int actorId, string langCode); Task> MultiSearch(string searchTerm, string languageCode); Task GetFullMovieInfo(int movieId, string langCode); + Task> DiscoverMovies(string langCode, int keywordId); + Task GetCollection(string langCode, int collectionId); } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/BelongsToCollection.cs b/src/Ombi.TheMovieDbApi/Models/BelongsToCollection.cs index abbc69fef..f48941d83 100644 --- a/src/Ombi.TheMovieDbApi/Models/BelongsToCollection.cs +++ b/src/Ombi.TheMovieDbApi/Models/BelongsToCollection.cs @@ -1,10 +1,16 @@ -namespace Ombi.TheMovieDbApi.Models +using Newtonsoft.Json; + +namespace Ombi.TheMovieDbApi.Models { public class BelongsToCollection { - public int id { get; set; } - public string name { get; set; } - public string poster_path { get; set; } - public string backdrop_path { get; set; } + [JsonProperty("id")] + public int Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("poster_path")] + public string PosterPath { get; set; } + [JsonProperty("backdrop_path")] + public string BackdropPath { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/Collections.cs b/src/Ombi.TheMovieDbApi/Models/Collections.cs new file mode 100644 index 000000000..951275180 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/Collections.cs @@ -0,0 +1,31 @@ +namespace Ombi.Api.TheMovieDb.Models +{ + public class Collections + { + public int id { get; set; } + public string name { get; set; } + public string overview { get; set; } + public string poster_path { get; set; } + public string backdrop_path { get; set; } + public Part[] parts { get; set; } + } + + public class Part + { + public bool adult { get; set; } + public string backdrop_path { get; set; } + public int[] genre_ids { get; set; } + public int id { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public string overview { get; set; } + public string poster_path { get; set; } + public string release_date { get; set; } + public string title { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public int vote_count { get; set; } + public float popularity { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/DiscoverMovies.cs b/src/Ombi.TheMovieDbApi/Models/DiscoverMovies.cs new file mode 100644 index 000000000..92f12cdc2 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/DiscoverMovies.cs @@ -0,0 +1,21 @@ +namespace Ombi.Api.TheMovieDb.Models { + + public class DiscoverMovies + { + public int vote_count { get; set; } + public int id { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public string title { get; set; } + public float popularity { get; set; } + public string poster_path { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public int[] genre_ids { get; set; } + public string backdrop_path { get; set; } + public bool adult { get; set; } + public string overview { get; set; } + public string release_date { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/FullMovieInfo.cs b/src/Ombi.TheMovieDbApi/Models/FullMovieInfo.cs index 58e05b3f4..151ad3399 100644 --- a/src/Ombi.TheMovieDbApi/Models/FullMovieInfo.cs +++ b/src/Ombi.TheMovieDbApi/Models/FullMovieInfo.cs @@ -1,4 +1,4 @@ -using System; +using System.Collections.Generic; using Newtonsoft.Json; using Ombi.TheMovieDbApi.Models; @@ -10,6 +10,8 @@ namespace Ombi.Api.TheMovieDb.Models public bool Adult { get; set; } [JsonProperty("backdrop_path")] public string BackdropPath { get; set; } + [JsonProperty("belongs_to_collection")] + public BelongsToCollection BelongsToCollection { get; set; } [JsonProperty("budget")] public int Budget { get; set; } [JsonProperty("genres")] @@ -66,6 +68,20 @@ namespace Ombi.Api.TheMovieDb.Models public ReleaseDates ReleaseDates { get; set; } [JsonProperty("external_ids")] public ExternalIds ExternalIds { get; set; } + [JsonProperty("keywords")] + public Keywords Keywords { get; set; } + } + + public class Keywords + { + [JsonProperty("keywords")] + public List KeywordsValue { get; set; } + } + + public class KeywordsValue + { + public int Id { get; set; } + public string Name { get; set; } } public class Videos diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 3bdc952ac..53cf5a05f 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using AutoMapper; using Ombi.Api.TheMovieDb.Models; -using Ombi.Helpers; using Ombi.TheMovieDbApi.Models; namespace Ombi.Api.TheMovieDb @@ -38,12 +37,34 @@ namespace Ombi.Api.TheMovieDb var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langCode); - request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,credits,similar,recommendations,release_dates,external_ids"); + request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,credits,similar,recommendations,release_dates,external_ids,keywords"); AddRetry(request); return await Api.Request(request); } + public async Task> DiscoverMovies(string langCode, int keywordId) + { + // https://developers.themoviedb.org/3/discover/movie-discover + var request = new Request("discover/movie", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + request.FullUri = request.FullUri.AddQueryParameter("language", langCode); + request.FullUri = request.FullUri.AddQueryParameter("with_keyword", keywordId.ToString()); + request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); + + return await Api.Request>(request); + } + + public async Task GetCollection(string langCode, int collectionId) + { + // https://developers.themoviedb.org/3/discover/movie-discover + var request = new Request($"collection/{collectionId}", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + request.FullUri = request.FullUri.AddQueryParameter("language", langCode); + + return await Api.Request (request); + } + public async Task Find(string externalId, ExternalSource source) { var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get); @@ -135,41 +156,57 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } - public async Task> PopularMovies(string langageCode) + public async Task> PopularMovies(string langageCode, int? page = null) { var request = new Request($"movie/popular", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + if (page != null) + { + request.FullUri = request.FullUri.AddQueryParameter("page", page.ToString()); + } AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - public async Task> TopRated(string langageCode) + public async Task> TopRated(string langageCode, int? page = null) { var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + if (page != null) + { + request.FullUri = request.FullUri.AddQueryParameter("page", page.ToString()); + } AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - public async Task> Upcoming(string langageCode) + public async Task> Upcoming(string langageCode, int? page = null) { var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + if (page != null) + { + request.FullUri = request.FullUri.AddQueryParameter("page", page.ToString()); + } AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - public async Task> NowPlaying(string langageCode) + public async Task> NowPlaying(string langageCode, int? page = null) { var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("language", langageCode); + if (page != null) + { + request.FullUri = request.FullUri.AddQueryParameter("page", page.ToString()); + } AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); diff --git a/src/Ombi.Updater/Ombi.Updater.csproj b/src/Ombi.Updater/Ombi.Updater.csproj index 6220a100b..0fdad5985 100644 --- a/src/Ombi.Updater/Ombi.Updater.csproj +++ b/src/Ombi.Updater/Ombi.Updater.csproj @@ -11,7 +11,7 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/src/Ombi.sln b/src/Ombi.sln index 70668294d..af8171bec 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28729.10 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi", "Ombi\Ombi.csproj", "{C987AA67-AFE1-468F-ACD3-EAD5A48E1F6A}" EndProject @@ -98,6 +98,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Lidarr", "Ombi.Api EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Helpers.Tests", "Ombi.Helpers.Tests\Ombi.Helpers.Tests.csproj", "{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Gotify", "Ombi.Api.Gotify\Ombi.Api.Gotify.csproj", "{105EA346-766E-45B8-928B-DE6991DCB7EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Settings.Tests", "Ombi.Settings.Tests\Ombi.Settings.Tests.csproj", "{F3969B69-3B07-4884-A7AB-0BAB8B84DF94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Test.Common", "Ombi.Test.Common\Ombi.Test.Common.csproj", "{27111E7C-748E-4996-BD71-2117027C6460}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Hubs", "Ombi.Hubs\Ombi.Hubs.csproj", "{67416CC5-13B2-44BB-98CE-39DA93D6F70E}" EndProject Global @@ -258,6 +264,18 @@ Global {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.Build.0 = Release|Any CPU + {105EA346-766E-45B8-928B-DE6991DCB7EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {105EA346-766E-45B8-928B-DE6991DCB7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.Build.0 = Release|Any CPU + {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Release|Any CPU.Build.0 = Release|Any CPU + {27111E7C-748E-4996-BD71-2117027C6460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27111E7C-748E-4996-BD71-2117027C6460}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27111E7C-748E-4996-BD71-2117027C6460}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27111E7C-748E-4996-BD71-2117027C6460}.Release|Any CPU.Build.0 = Release|Any CPU {67416CC5-13B2-44BB-98CE-39DA93D6F70E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {67416CC5-13B2-44BB-98CE-39DA93D6F70E}.Debug|Any CPU.Build.0 = Debug|Any CPU {67416CC5-13B2-44BB-98CE-39DA93D6F70E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -299,6 +317,9 @@ Global {10D1FE9D-9124-42B7-B1E1-CEB99B832618} = {9293CA11-360A-4C20-A674-B9E794431BF5} {4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5} {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1} + {105EA346-766E-45B8-928B-DE6991DCB7EB} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {F3969B69-3B07-4884-A7AB-0BAB8B84DF94} = {6F42AB98-9196-44C4-B888-D5E409F415A1} + {27111E7C-748E-4996-BD71-2117027C6460} = {6F42AB98-9196-44C4-B888-D5E409F415A1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869} diff --git a/src/Ombi/.vscode/launch.json b/src/Ombi/.vscode/launch.json index de90bc712..0a45aaea0 100644 --- a/src/Ombi/.vscode/launch.json +++ b/src/Ombi/.vscode/launch.json @@ -1,41 +1,27 @@ { "version": "0.2.0", "configurations": [ - { - "name": ".NET Core Launch", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Ombi.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "launchBrowser": { - "enabled": true, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/Views" - } - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:3578/", + "webRoot": "${workspaceFolder}" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "url": "http://localhost:9876/debug.html", + "webRoot": "${workspaceFolder}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "/./*": "${webRoot}/*", + "/src/*": "${webRoot}/*", + "/*": "*", + "/./~/*": "${webRoot}/node_modules/*" } + }, ] -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Ombi/.vscode/settings.json b/src/Ombi/.vscode/settings.json index 357eeb5bf..b2005042c 100644 --- a/src/Ombi/.vscode/settings.json +++ b/src/Ombi/.vscode/settings.json @@ -9,5 +9,6 @@ "typescript.tsdk": "node_modules\\typescript\\lib", "cSpell.words": [ "usermanagement" - ] + ], + "discord.enabled": true } diff --git a/src/Ombi/ApiKeyMiddlewear.cs b/src/Ombi/ApiKeyMiddlewear.cs index f3c956df4..e8fa02d78 100644 --- a/src/Ombi/ApiKeyMiddlewear.cs +++ b/src/Ombi/ApiKeyMiddlewear.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Ombi.Core.Authentication; using Ombi.Core.Settings; +using Ombi.Helpers; using Ombi.Settings.Settings.Models; namespace Ombi @@ -98,6 +99,10 @@ namespace Ombi if (context.Request.Headers.Keys.Contains("UserName", StringComparer.InvariantCultureIgnoreCase)) { var username = context.Request.Headers["UserName"].FirstOrDefault(); + if (username.IsNullOrEmpty()) + { + UseApiUser(context); + } var um = context.RequestServices.GetService(); var user = await um.Users.FirstOrDefaultAsync(x => x.UserName.Equals(username, StringComparison.InvariantCultureIgnoreCase)); @@ -114,13 +119,18 @@ namespace Ombi } else { - var identity = new GenericIdentity("API"); - var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" }); - context.User = principal; + UseApiUser(context); } await next.Invoke(context); } } + + private void UseApiUser(HttpContext context) + { + var identity = new GenericIdentity("API"); + var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" }); + context.User = principal; + } } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/angular.json b/src/Ombi/ClientApp/angular.json index a01be1215..ecad176c9 100644 --- a/src/Ombi/ClientApp/angular.json +++ b/src/Ombi/ClientApp/angular.json @@ -27,12 +27,18 @@ "src/styles/_imports.scss", "node_modules/bootstrap/scss/bootstrap.scss", "node_modules/angular-bootstrap-md/scss/mdb-free.scss", - "node_modules/font-awesome/scss/font-awesome.scss" + "node_modules/pace/themes/orange/pace-theme-flat-top.css", + "node_modules/font-awesome/scss/font-awesome.scss", + "node_modules/primeng/resources/primeng.min.css", + "node_modules/primeng/resources/themes/nova-light/theme.css", + "node_modules/primeicons/primeicons.css", + "node_modules/fullcalendar/dist/fullcalendar.min.css" ], "scripts": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/chart.js/dist/Chart.js", - "node_modules/hammerjs/hammer.min.js" + "node_modules/hammerjs/hammer.min.js", + "node_modules/fullcalendar/dist/fullcalendar.min.js" ] }, "configurations": { diff --git a/src/Ombi/ClientApp/package.json b/src/Ombi/ClientApp/package.json index f2515bc07..27bca4ec1 100644 --- a/src/Ombi/ClientApp/package.json +++ b/src/Ombi/ClientApp/package.json @@ -40,6 +40,7 @@ "core-js": "^2.5.4", "eventemitter2": "^5.0.1", "font-awesome": "^4.7.0", + "fullcalendar": "4.0.0-alpha.2", "hammerjs": "^2.0.8", "jquery": "3.3.1", "moment": "^2.23.0", @@ -47,12 +48,13 @@ "ngx-bootstrap": "^3.1.4", "ngx-clipboard": "^11.1.1", "ngx-editor": "^4.1.0", - "ngx-infinite-scroll": "^6.0.1", + "ngx-infinite-scroll": "^7.1.0", "ngx-moment": "^3.0.1", "ngx-order-pipe": "^2.0.1", "ngx-page-scroll": "^5.0.1", "pace": "github:HubSpot/pace#v1.0.2", "popper.js": "^1.14.3", + "primeicons": "^1.0.0", "primeng": "^7.0.3", "rxjs": "^6.0.0", "socket.io-client": "^2.2.0", diff --git a/src/Ombi/ClientApp/src/app/app.component.html b/src/Ombi/ClientApp/src/app/app.component.html index b5229d2a4..635693765 100644 --- a/src/Ombi/ClientApp/src/app/app.component.html +++ b/src/Ombi/ClientApp/src/app/app.component.html @@ -1,4 +1,4 @@ -
+ - + + + +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/app.component.ts b/src/Ombi/ClientApp/src/app/app.component.ts index cc0ae6542..706570de5 100644 --- a/src/Ombi/ClientApp/src/app/app.component.ts +++ b/src/Ombi/ClientApp/src/app/app.component.ts @@ -1,14 +1,16 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { OverlayContainer } from '@angular/cdk/overlay'; + +import { Component, OnInit, HostBinding } from "@angular/core"; import { NavigationStart, Router } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { AuthService } from "./auth/auth.service"; import { ILocalUser } from "./auth/IUserLogin"; -import { IdentityService, NotificationService } from "./services"; +import { IdentityService, NotificationService, CustomPageService } from "./services"; import { JobService, SettingsService } from "./services"; import { MatSnackBar } from '@angular/material'; import { ICustomizationSettings, ICustomPage } from "./interfaces"; +import { StorageService } from './shared/storage/storage-service'; import { HubConnection } from '@aspnet/signalr'; import * as signalR from '@aspnet/signalr'; @@ -30,9 +32,15 @@ export class AppComponent implements OnInit { public currentUrl: string; public userAccessToken: string; public voteEnabled = false; + public applicationName: string = "Ombi" + public isAdmin: boolean; + public username: string; private checkedForUpdate: boolean; + + @HostBinding('class') public componentCssClass; + private scheduleHubConnection: HubConnection | undefined; constructor(public notificationService: NotificationService, @@ -42,30 +50,44 @@ export class AppComponent implements OnInit { private readonly jobService: JobService, public readonly translate: TranslateService, private readonly identityService: IdentityService, - private readonly platformLocation: PlatformLocation, + private readonly customPageService: CustomPageService, + public overlayContainer: OverlayContainer, + private storage: StorageService, private readonly snackBar: MatSnackBar) { - const base = this.platformLocation.getBaseHrefFromDOM(); - if (base.length > 1) { - __webpack_public_path__ = base + "/dist/"; - } + // const base = this.platformLocation.getBaseHrefFromDOM(); + // if (base.length > 1) { + // __webpack_public_path__ = base + "/dist/"; + // } this.translate.addLangs(["en", "de", "fr", "da", "es", "it", "nl", "sv", "no", "pl", "pt"]); + + const selectedLang = this.storage.get("Language"); + // this language will be used as a fallback when a translation isn't found in the current language this.translate.setDefaultLang("en"); - - // See if we can match the supported langs with the current browser lang - const browserLang: string = translate.getBrowserLang(); - this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|sv|no|pl|pt/) ? browserLang : "en"); + if (selectedLang) { + this.translate.use(selectedLang); + } else { + // See if we can match the supported langs with the current browser lang + const browserLang: string = translate.getBrowserLang(); + this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|sv|no|pl|pt/) ? browserLang : "en"); + } } public ngOnInit() { - this.user = this.authService.claims(); + const theme = this.storage.get("theme"); + this.onSetTheme(theme); this.settingsService.getCustomization().subscribe(x => { this.customizationSettings = x; + + if (this.customizationSettings && this.customizationSettings.applicationName) { + this.applicationName = this.customizationSettings.applicationName; + } + if (this.customizationSettings.useCustomPage) { - this.settingsService.getCustomPage().subscribe(c => { + this.customPageService.getCustomPage().subscribe(c => { this.customPageSettings = c; if (!this.customPageSettings.title) { this.customPageSettings.title = "Custom Page"; @@ -81,10 +103,14 @@ export class AppComponent implements OnInit { this.currentUrl = event.url; if (event instanceof NavigationStart) { this.user = this.authService.claims(); + if (this.user && this.user.username) { + this.username = this.user.username; + } + this.isAdmin = this.authService.hasRole("admin"); this.showNav = this.authService.loggedIn(); // tslint:disable-next-line:no-string-literal - if (this.user !== null && this.user.name && !this.checkedForUpdate && this.user.roles["Admin"]) { + if (this.user !== null && this.user.name && !this.checkedForUpdate && this.isAdmin) { this.checkedForUpdate = true; this.jobService.getCachedUpdate().subscribe(x => { this.updateAvailable = x; @@ -113,19 +139,6 @@ export class AppComponent implements OnInit { } - public roleClass() { - if (this.user.roles.some(r => r === "Admin")) { - return "adminUser"; - } else if (this.user.roles.some(r => r === "PowerUser")) { - return "powerUser"; - } - return "user"; - } - - public hasRole(role: string): boolean { - return this.user.roles.some(r => r === role); - } - public openMobileApp(event: any) { event.preventDefault(); if (!this.customizationSettings.applicationUrl) { @@ -143,4 +156,11 @@ export class AppComponent implements OnInit { this.authService.logout(); this.router.navigate(["login"]); } + + public onSetTheme(theme: string) { + if (theme) { + this.overlayContainer.getContainerElement().classList.add(theme); + this.componentCssClass = theme; + } + } } diff --git a/src/Ombi/ClientApp/src/app/app.module.ts b/src/Ombi/ClientApp/src/app/app.module.ts index 2949c833c..502bee92a 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { CommonModule, PlatformLocation } from "@angular/common"; +import { CommonModule, PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient, HttpClientModule } from "@angular/common/http"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; @@ -14,12 +14,15 @@ import { TranslateHttpLoader } from "@ngx-translate/http-loader"; import { CookieService } from "ng2-cookies"; import { NgxEditorModule } from "ngx-editor"; import { GrowlModule } from "primeng/components/growl/growl"; -import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, OverlayPanelModule, SharedModule, SidebarModule, - TooltipModule } from "primeng/primeng"; +import { + ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, OverlayPanelModule, SharedModule, SidebarModule, + TooltipModule +} from "primeng/primeng"; - import { - MatButtonModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatAutocompleteModule, MatCheckboxModule, MatSnackBarModule} from '@angular/material'; - import { MatCardModule, MatInputModule, MatTabsModule } from "@angular/material"; +import { + MatButtonModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatAutocompleteModule, MatCheckboxModule, MatSnackBarModule +} from '@angular/material'; +import { MatCardModule, MatInputModule, MatTabsModule, MatSlideToggleModule } from "@angular/material"; import { MDBBootstrapModule, CardsFreeModule, NavbarModule } from "angular-bootstrap-md"; @@ -38,15 +41,17 @@ import { TokenResetPasswordComponent } from "./login/tokenresetpassword.componen // Services import { AuthGuard } from "./auth/auth.guard"; import { AuthService } from "./auth/auth.service"; -import { ImageService } from "./services"; +import { ImageService, SettingsService, CustomPageService } from "./services"; import { LandingPageService } from "./services"; import { NotificationService } from "./services"; -import { SettingsService } from "./services"; -import { IssuesService, JobService, PlexTvService, StatusService, SearchService, IdentityService } from "./services"; +import { IssuesService, JobService, PlexTvService, StatusService, SearchService, IdentityService, MessageService } from "./services"; import { MyNavComponent } from './my-nav/my-nav.component'; import { LayoutModule } from '@angular/cdk/layout'; import { SearchV2Service } from "./services/searchV2.service"; import { NavSearchComponent } from "./my-nav/nav-search.component"; +import { OverlayModule } from "@angular/cdk/overlay"; +import { getBaseLocation } from "./shared/functions/common-functions"; +import { StorageService } from "./shared/storage/storage-service"; const routes: Routes = [ { path: "*", component: PageNotFoundComponent }, @@ -59,20 +64,24 @@ const routes: Routes = [ { path: "token", component: TokenResetPasswordComponent }, { path: "landingpage", component: LandingPageComponent }, { path: "auth/cookie", component: CookieComponent }, + { loadChildren: "./calendar/calendar.module#CalendarModule", path: "calendar" }, { loadChildren: "./discover/discover.module#DiscoverModule", path: "discover" }, { loadChildren: "./issues/issues.module#IssuesModule", path: "issues" }, { loadChildren: "./settings/settings.module#SettingsModule", path: "Settings" }, { loadChildren: "./wizard/wizard.module#WizardModule", path: "Wizard" }, { loadChildren: "./usermanagement/usermanagement.module#UserManagementModule", path: "usermanagement" }, - { loadChildren: "./requests/requests.module#RequestsModule", path: "requests" }, + { loadChildren: "./requests/requests.module#RequestsModule", path: "requestsOld" }, + { loadChildren: "./requests-list/requests-list.module#RequestsListModule", path: "requests-list" }, { loadChildren: "./search/search.module#SearchModule", path: "search" }, { loadChildren: "./recentlyAdded/recentlyAdded.module#RecentlyAddedModule", path: "recentlyadded" }, { loadChildren: "./vote/vote.module#VoteModule", path: "vote" }, { loadChildren: "./media-details/media-details.module#MediaDetailsModule", path: "details" }, + { loadChildren: "./user-preferences/user-preferences.module#UserPreferencesModule", path: "user-preferences" }, ]; // AoT requires an exported function for factories export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLocation) { + // const base = getBaseLocation(); const base = platformLocation.getBaseHrefFromDOM(); const version = Math.floor(Math.random() * 999999999); if (base.length > 1) { @@ -89,6 +98,7 @@ export function JwtTokenGetter() { return token; } + @NgModule({ imports: [ RouterModule.forRoot(routes), @@ -100,6 +110,7 @@ export function JwtTokenGetter() { ButtonModule, FormsModule, DataTableModule, + MatSnackBarModule, SharedModule, NgxEditorModule, MatSnackBarModule, @@ -118,6 +129,7 @@ export function JwtTokenGetter() { OverlayPanelModule, CommonModule, CardsFreeModule, + OverlayModule, MatCheckboxModule, MDBBootstrapModule.forRoot(), JwtModule.forRoot({ @@ -133,7 +145,7 @@ export function JwtTokenGetter() { }, }), SidebarModule, - MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, LayoutModule, + MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, LayoutModule, MatSlideToggleModule ], declarations: [ AppComponent, @@ -158,13 +170,21 @@ export function JwtTokenGetter() { LandingPageService, ConfirmationService, ImageService, + CustomPageService, CookieService, JobService, IssuesService, PlexTvService, SearchService, SearchV2Service, - ], + MessageService, + StorageService, + { provide: APP_BASE_HREF, useValue: window['_app_base'] || '/' } + // { + // provide: APP_BASE_HREF, + // useFactory: getBaseLocation + // } + ], bootstrap: [AppComponent], }) export class AppModule { } diff --git a/src/Ombi/ClientApp/src/app/auth/IUserLogin.ts b/src/Ombi/ClientApp/src/app/auth/IUserLogin.ts index d0e4d374a..4a24798d4 100644 --- a/src/Ombi/ClientApp/src/app/auth/IUserLogin.ts +++ b/src/Ombi/ClientApp/src/app/auth/IUserLogin.ts @@ -11,4 +11,5 @@ export interface IUserLogin { export interface ILocalUser { roles: string[]; name: string; + username:string; } diff --git a/src/Ombi/ClientApp/src/app/auth/auth.guard.ts b/src/Ombi/ClientApp/src/app/auth/auth.guard.ts index d06d8c590..4313587ab 100644 --- a/src/Ombi/ClientApp/src/app/auth/auth.guard.ts +++ b/src/Ombi/ClientApp/src/app/auth/auth.guard.ts @@ -3,17 +3,19 @@ import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; import { CanActivate } from "@angular/router"; import { AuthService } from "./auth.service"; +import { StorageService } from "../shared/storage/storage-service"; @Injectable() export class AuthGuard implements CanActivate { - constructor(private auth: AuthService, private router: Router) { } + constructor(private auth: AuthService, private router: Router, + private store: StorageService) { } public canActivate() { if (this.auth.loggedIn()) { return true; } else { - localStorage.removeItem("token"); + this.store.remove("token"); this.router.navigate(["login"]); return false; } diff --git a/src/Ombi/ClientApp/src/app/auth/auth.service.ts b/src/Ombi/ClientApp/src/app/auth/auth.service.ts index 3f9768c71..8f3c6af29 100644 --- a/src/Ombi/ClientApp/src/app/auth/auth.service.ts +++ b/src/Ombi/ClientApp/src/app/auth/auth.service.ts @@ -1,29 +1,31 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { JwtHelperService } from "@auth0/angular-jwt"; import { Observable } from "rxjs"; import { ServiceHelpers } from "../services"; import { ILocalUser, IUserLogin } from "./IUserLogin"; +import { StorageService } from "../shared/storage/storage-service"; @Injectable() export class AuthService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation, private jwtHelperService: JwtHelperService) { - super(http, "/api/v1/token", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href: string, private jwtHelperService: JwtHelperService, + private store: StorageService) { + super(http, "/api/v1/token", href); } public login(login: IUserLogin): Observable { - return this.http.post(`${this.url}/`, JSON.stringify(login), {headers: this.headers}); + return this.http.post(`${this.url}/`, JSON.stringify(login), { headers: this.headers }); } public oAuth(pin: number): Observable { - return this.http.get(`${this.url}/${pin}`, {headers: this.headers}); + return this.http.get(`${this.url}/${pin}`, { headers: this.headers }); } public requiresPassword(login: IUserLogin): Observable { - return this.http.post(`${this.url}/requirePassword`, JSON.stringify(login), {headers: this.headers}); + return this.http.post(`${this.url}/requirePassword`, JSON.stringify(login), { headers: this.headers }); } public getToken() { @@ -43,7 +45,7 @@ export class AuthService extends ServiceHelpers { public claims(): ILocalUser { if (this.loggedIn()) { - const token = localStorage.getItem("id_token"); + const token = this.store.get("id_token"); if (!token) { throw new Error("Invalid token"); } @@ -53,20 +55,25 @@ export class AuthService extends ServiceHelpers { const u = { name, roles: [] as string[] }; if (roles instanceof Array) { - u.roles = roles; + u.roles = roles; } else { u.roles.push(roles); } - return u; + return u; } - return { }; + return {}; } public hasRole(role: string): boolean { - return this.claims().roles.some(r => r.toUpperCase() === role.toUpperCase()); + const claims = this.claims(); + + if (claims && claims.roles && role) { + return claims.roles.some(r => r.toUpperCase() === role.toUpperCase()); + } + return false; } public logout() { - localStorage.removeItem("id_token"); + this.store.remove("id_token"); } } diff --git a/src/Ombi/ClientApp/src/app/auth/cookie.component.ts b/src/Ombi/ClientApp/src/app/auth/cookie.component.ts index 21b61b37e..515f8c603 100644 --- a/src/Ombi/ClientApp/src/app/auth/cookie.component.ts +++ b/src/Ombi/ClientApp/src/app/auth/cookie.component.ts @@ -1,19 +1,21 @@ import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { CookieService } from "ng2-cookies"; +import { StorageService } from "../shared/storage/storage-service"; @Component({ templateUrl: "cookie.component.html", }) export class CookieComponent implements OnInit { constructor(private readonly cookieService: CookieService, - private readonly router: Router) { } + private readonly router: Router, + private store: StorageService) { } public ngOnInit() { const cookie = this.cookieService.getAll(); if (cookie.Auth) { const jwtVal = cookie.Auth; - localStorage.setItem("id_token", jwtVal); + this.store.save("id_token", jwtVal); this.router.navigate(["search"]); } else { this.router.navigate(["login"]); diff --git a/src/Ombi/ClientApp/src/app/calendar/calendar.module.ts b/src/Ombi/ClientApp/src/app/calendar/calendar.module.ts new file mode 100644 index 000000000..89cab9dd0 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/calendar/calendar.module.ts @@ -0,0 +1,39 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { RequestService } from "../services"; + +import { SharedModule } from "../shared/shared.module"; +import { PipeModule } from "../pipes/pipe.module"; + +import * as fromComponents from './components'; +import { AuthGuard } from "../auth/auth.guard"; +import { CalendarComponent } from "./components/calendar.component"; + +import { FullCalendarModule } from 'primeng/fullcalendar'; +import { CalendarService } from "../services/calendar.service"; + + +const routes: Routes = [ + { path: "", component: CalendarComponent, canActivate: [AuthGuard] }, +]; +@NgModule({ + imports: [ + RouterModule.forChild(routes), + SharedModule, + PipeModule, + FullCalendarModule, + ], + declarations: [ + ...fromComponents.components + ], + exports: [ + RouterModule, + ], + providers: [ + RequestService, + CalendarService + ], + +}) +export class CalendarModule { } diff --git a/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.html b/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.html new file mode 100644 index 000000000..47c6213db --- /dev/null +++ b/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.scss b/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.scss new file mode 100644 index 000000000..195b7ff32 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.scss @@ -0,0 +1,5 @@ + +.small-middle-container{ + margin: auto; + width: 80%; +} diff --git a/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.ts b/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.ts new file mode 100644 index 000000000..0a183651a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/calendar/components/calendar.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from "@angular/core"; + +import { CalendarService } from "../../services/calendar.service"; +import { ICalendarModel } from "../../interfaces/ICalendar"; + +@Component({ + templateUrl: "./calendar.component.html", + styleUrls: ["./calendar.component.scss"], +}) +export class CalendarComponent implements OnInit { + + public loadingFlag: boolean; + events: any[]; + options: any; + entries: ICalendarModel[]; + + constructor(private calendarService: CalendarService) { } + + public async ngOnInit() { + debugger; + this.loading() + this.entries = await this.calendarService.getCalendarEntries(); + + this.options = { + defaultDate: new Date(), + header: { + left: 'prev,next', + center: 'title', + right: 'month,agendaWeek' + }, + eventClick: (e: any) => { + debugger; + e.preventDefault(); + } + }; + this.finishLoading(); + } + + private loading() { + this.loadingFlag = true; + } + + private finishLoading() { + this.loadingFlag = false; + } +} diff --git a/src/Ombi/ClientApp/src/app/calendar/components/index.ts b/src/Ombi/ClientApp/src/app/calendar/components/index.ts new file mode 100644 index 000000000..74f27ba63 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/calendar/components/index.ts @@ -0,0 +1,5 @@ +import { CalendarComponent } from "./calendar.component"; + +export const components: any[] = [ + CalendarComponent, +]; diff --git a/src/Ombi/ClientApp/src/app/custompage/custompage.component.ts b/src/Ombi/ClientApp/src/app/custompage/custompage.component.ts index f346bef2b..61094e793 100644 --- a/src/Ombi/ClientApp/src/app/custompage/custompage.component.ts +++ b/src/Ombi/ClientApp/src/app/custompage/custompage.component.ts @@ -2,7 +2,7 @@ import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { DomSanitizer } from "@angular/platform-browser"; import { AuthService } from "../auth/auth.service"; -import { NotificationService, SettingsService } from "../services"; +import { CustomPageService, NotificationService } from "../services"; @Component({ templateUrl: "./custompage.component.html", @@ -14,7 +14,7 @@ export class CustomPageComponent implements OnInit { public isEditing: boolean; public isAdmin: boolean; - constructor(private auth: AuthService, private settings: SettingsService, private fb: FormBuilder, + constructor(private auth: AuthService, private settings: CustomPageService, private fb: FormBuilder, private notificationService: NotificationService, private sanitizer: DomSanitizer) { } @@ -29,7 +29,7 @@ export class CustomPageComponent implements OnInit { fontAwesomeIcon: [x.fontAwesomeIcon, [Validators.required]], }); }); - this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + this.isAdmin = this.auth.hasRole("EditCustomPage"); } public onSubmit() { diff --git a/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.html b/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.html index f6072be5f..63dee260c 100644 --- a/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.html +++ b/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.html @@ -1,10 +1,46 @@ -
+
+ +
+
+
- {{data.title}} + + {{data.title}} +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +

{{data.title}}

@@ -13,22 +49,50 @@
- Availability: Unavailable + {{'Discovery.CardDetails.Availability' | translate}}: + + +
- Studio: Studio1 + {{'Discovery.CardDetails.Studio' | translate}}: + {{movie.productionCompanies[0].name}} + {{'Discovery.CardDetails.Network' | translate}}: + {{tv.network.name}} + {{'Discovery.CardDetails.UnknownNetwork' | translate}} +
+
+ {{'Discovery.CardDetails.RequestStatus' | translate}}: + + + + +
- Request Status: Studio1 + {{'Discovery.CardDetails.Director' | translate}}: + {{movie.credits.crew[0].name}} + Director: + {{tvCreator}}
- Director: Studio1 + {{'Discovery.CardDetails.InCinemas' | translate}}: + {{movie.releaseDate | amLocal | amDateFormat: 'LL'}} + {{'Discovery.CardDetails.FirstAired' | translate}}: + {{tv.firstAired | amLocal | amDateFormat: 'LL'}}
- In Cinemas: Studio1 -
-
- Writer: Studio1 + {{'Discovery.CardDetails.Writer' | translate}}: + {{movie.credits.crew[1].name}} + {{'Discovery.CardDetails.ExecProducer' | translate}}: + {{tvProducer}}
@@ -45,12 +109,51 @@
-
- +
+ + + + + + + + + + +
+ +
+ + + + + + + + {{'Search.ViewOnPlex' | + translate}} + {{'Search.ViewOnEmby' | + translate}} + + +
-
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.scss b/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.scss index 261093db2..bdec69c55 100644 --- a/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.scss +++ b/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.scss @@ -1,31 +1,46 @@ -$ombi-primary:#3f3f3f; -$ombi-primary-700:#313131; -$ombi-accent-700:#166d50; +@import "~styles/variables.scss"; .poster { max-width: 100%; border-radius: 2%; } .details { - background: $ombi-primary-700; padding: 2%; border-radius: 10px; + background: $backgroundTint; + div.dark & { + background: $backgroundTint-dark; + } } .details strong { font-weight: bold; } - -h3 strong { - font-weight: bold; +.grow { + transition: all .2s ease-in-out; } -.request-btn { - background-color: $ombi-accent-700; - color: white; +.grow:hover { + transform: scale(1.1); +} +h3 strong { + font-weight: bold; } .action-buttons-right { width: 100%; text-align: right; -} \ No newline at end of file +} + +.btn-spacing { + margin-right: 1%; +} + +.media-icons { + color: $primary; + padding: 2%; + div.dark & { + color: $warn-dark; + } +} + diff --git a/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.ts b/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.ts index ebe19f87f..a92c2f961 100644 --- a/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/card/discover-card-details.component.ts @@ -1,18 +1,86 @@ -import { Component, Inject } from "@angular/core"; -import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; +import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from "@angular/material"; import { IDiscoverCardResult } from "../interfaces"; +import { SearchV2Service, RequestService, MessageService } from "../../services"; +import { RequestType } from "../../interfaces"; +import { ISearchMovieResultV2 } from "../../interfaces/ISearchMovieResultV2"; +import { ISearchTvResultV2 } from "../../interfaces/ISearchTvResultV2"; +import { RouterLink, Router } from "@angular/router"; +import { EpisodeRequestComponent } from "../../shared/episode-request/episode-request.component"; @Component({ selector: "discover-card-details", templateUrl: "./discover-card-details.component.html", styleUrls: ["./discover-card-details.component.scss"], + encapsulation: ViewEncapsulation.None, }) -export class DiscoverCardDetailsComponent { +export class DiscoverCardDetailsComponent implements OnInit { + + public movie: ISearchMovieResultV2; + public tv: ISearchTvResultV2; + public tvCreator: string; + public tvProducer: string; + public loading: boolean; + public RequestType = RequestType; + constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: IDiscoverCardResult) { } - - onNoClick(): void { + @Inject(MAT_DIALOG_DATA) public data: IDiscoverCardResult, private searchService: SearchV2Service, private dialog: MatDialog, + private requestService: RequestService, public messageService: MessageService, private router: Router) { } + + public async ngOnInit() { + this.loading = true; + if (this.data.type === RequestType.movie) { + this.movie = await this.searchService.getFullMovieDetailsPromise(this.data.id); + } else if (this.data.type === RequestType.tvShow) { + this.tv = await this.searchService.getTvInfo(this.data.id); + const creator = this.tv.crew.filter(tv => { + return tv.type === "Creator"; + })[0]; + if (creator && creator.person) { + this.tvCreator = creator.person.name; + } + const crewResult = this.tv.crew.filter(tv => { + return tv.type === "Executive Producer"; + })[0] + if (crewResult && crewResult.person) { + this.tvProducer = crewResult.person.name; + } + } + this.loading = false; + } + + public close(): void { this.dialogRef.close(); - } + } + + public openDetails() { + if (this.data.type === RequestType.movie) { + this.router.navigate(['/details/movie/', this.data.id]); + } else if (this.data.type === RequestType.tvShow) { + this.router.navigate(['/details/tv/', this.data.id]); + } + + this.dialogRef.close(); + } + + public async request() { + this.loading = true; + if (this.data.type === RequestType.movie) { + const result = await this.requestService.requestMovie({ theMovieDbId: this.data.id, languageCode: "" }).toPromise(); + this.loading = false; + + if (result.result) { + this.movie.requested = true; + this.messageService.send(result.message, "Ok"); + } else { + this.messageService.send(result.errorMessage, "Ok"); + } + } else if (this.data.type === RequestType.tvShow) { + this.dialog.open(EpisodeRequestComponent, { width: "700px", data: this.tv, panelClass: 'modal-panel' }) + } + this.loading = false; + + this.dialogRef.close(); + } } diff --git a/src/Ombi/ClientApp/src/app/discover/card/discover-card.component.html b/src/Ombi/ClientApp/src/app/discover/card/discover-card.component.html index efb6571dc..e4816df02 100644 --- a/src/Ombi/ClientApp/src/app/discover/card/discover-card.component.html +++ b/src/Ombi/ClientApp/src/app/discover/card/discover-card.component.html @@ -7,7 +7,9 @@
{{result.title}}
{{result.title | truncate:13}}
- {{result.overview | truncate: 50}} +
+ {{result.overview | truncate: 50}} +
+
+
+ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/collections/discover-collections.component.scss b/src/Ombi/ClientApp/src/app/discover/collections/discover-collections.component.scss new file mode 100644 index 000000000..728ff23c5 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/collections/discover-collections.component.scss @@ -0,0 +1,19 @@ +.full-height { + height: 100%; +} + + +.small-middle-container{ + margin: auto; + width: 80%; +} + +.small-padding { + padding-left: 20px; + padding-right: 20px; + margin-bottom: 28px; +} + +.loading-spinner { + margin: 10%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/collections/discover-collections.component.ts b/src/Ombi/ClientApp/src/app/discover/collections/discover-collections.component.ts new file mode 100644 index 000000000..872ffc9cc --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/collections/discover-collections.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { SearchV2Service, RequestService, NotificationService, MessageService } from "../../services"; +import { IMovieCollectionsViewModel } from "../../interfaces/ISearchTvResultV2"; +import { IDiscoverCardResult } from "../interfaces"; +import { RequestType } from "../../interfaces"; + +@Component({ + templateUrl: "./discover-collections.component.html", + styleUrls: ["./discover-collections.component.scss"], +}) +export class DiscoverCollectionsComponent implements OnInit { + + public collectionId: number; + public collection: IMovieCollectionsViewModel; + public loadingFlag: boolean; + + public discoverResults: IDiscoverCardResult[] = []; + + constructor(private searchService: SearchV2Service, + private route: ActivatedRoute, + private requestService: RequestService, + private messageService: MessageService) { + this.route.params.subscribe((params: any) => { + this.collectionId = params.collectionId; + }); + } + + public async ngOnInit() { + this.loadingFlag = true; + this.collection = await this.searchService.getMovieCollections(this.collectionId); + this.createModel(); + } + + public async requestCollection() { + await this.collection.collection.forEach(async (movie) => { + await this.requestService.requestMovie({theMovieDbId: movie.id, languageCode: null}).toPromise(); + }); + this.messageService.send("Requested Collection"); + } + + private createModel() { + this.finishLoading(); + this.collection.collection.forEach(m => { + this.discoverResults.push({ + available: m.available, + posterPath: `https://image.tmdb.org/t/p/w300/${m.posterPath}`, + requested: m.requested, + title: m.title, + type: RequestType.movie, + id: m.id, + url: `http://www.imdb.com/title/${m.imdbId}/`, + rating: 0, + overview: m.overview, + approved: m.approved + }); + }); + } + + private loading() { + this.loadingFlag = true; + } + + private finishLoading() { + this.loadingFlag = false; + } +} diff --git a/src/Ombi/ClientApp/src/app/discover/discover.component.html b/src/Ombi/ClientApp/src/app/discover/discover.component.html index 72b5879c1..8d1984d8f 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.component.html +++ b/src/Ombi/ClientApp/src/app/discover/discover.component.html @@ -1,7 +1,30 @@
-
-
- -
+ +
+
+ + +
-
\ No newline at end of file +
+ +
+
+ +
+
+
+ +
+
diff --git a/src/Ombi/ClientApp/src/app/discover/discover.component.scss b/src/Ombi/ClientApp/src/app/discover/discover.component.scss index 4fc5ac7d6..8e05d0955 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.component.scss +++ b/src/Ombi/ClientApp/src/app/discover/discover.component.scss @@ -5,10 +5,19 @@ .small-middle-container{ margin: auto; - width: 95%; + width: 80%; } .small-padding { - padding-left: 5px; - padding-right: 5px; -} \ No newline at end of file + padding-left: 20px; + padding-right: 20px; + margin-bottom: 28px; +} + +.loading-spinner { + margin: 10%; +} +#scroller { + height: 100vh; + overflow: scroll; + } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/discover.component.ts b/src/Ombi/ClientApp/src/app/discover/discover.component.ts index 45f4709a1..e1935f2f4 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/discover.component.ts @@ -10,27 +10,158 @@ import { trigger, transition, style, animate } from "@angular/animations"; animations: [ trigger('slideIn', [ transition(':enter', [ - style({transform: 'translateX(100%)'}), - animate('200ms ease-in', style({transform: 'translateY(0%)'})) + style({ transform: 'translateX(100%)' }), + animate('200ms ease-in', style({ transform: 'translateY(0%)' })) ]) - ]) + ]) ], }) export class DiscoverComponent implements OnInit { public discoverResults: IDiscoverCardResult[] = []; - private movies: ISearchMovieResult[]; - private tvShows: ISearchTvResult[]; + public movies: ISearchMovieResult[]; + public tvShows: ISearchTvResult[]; public defaultTvPoster: string; - constructor(private searchService: SearchV2Service) { + public popularActive: boolean = true; + public trendingActive: boolean; + public upcomingActive: boolean; + + public loadingFlag: boolean; + public scrollDisabled: boolean; + + private contentLoaded: number; + private isScrolling: boolean = false; + + constructor(private searchService: SearchV2Service) { } - } public async ngOnInit() { - this.movies = await this.searchService.popularMovies().toPromise(); - this.tvShows = await this.searchService.popularTv().toPromise(); + this.loading() + this.scrollDisabled = true; + this.movies = await this.searchService.popularMoviesByPage(0,12); + this.tvShows = await this.searchService.popularTvByPage(0,12); + this.contentLoaded = 12; + + this.createInitialModel(); + this.scrollDisabled = false; + } + + public async onScroll() { + if (!this.contentLoaded) { + return; + } + if (!this.isScrolling) { + debugger; + this.isScrolling = true; + console.log("SCROLLED!") + this.loading(); + if (this.popularActive) { + this.movies = await this.searchService.popularMoviesByPage(this.contentLoaded, 12); + this.tvShows = await this.searchService.popularTvByPage(this.contentLoaded, 12); + } + if(this.trendingActive) { + this.movies = await this.searchService.nowPlayingMoviesByPage(this.contentLoaded, 12); + this.tvShows = await this.searchService.trendingTvByPage(this.contentLoaded, 12); + } + if(this.upcomingActive) { + this.movies = await this.searchService.upcomingMoviesByPage(this.contentLoaded, 12); + this.tvShows = await this.searchService.anticipatedTvByPage(this.contentLoaded, 12); + } + this.contentLoaded += 12; + + this.createModel(); + this.isScrolling = false; + } + } + + public async popular() { + this.clear(); + this.scrollDisabled = true; + this.isScrolling = false; + this.contentLoaded = 12; + this.loading() + this.popularActive = true; + this.trendingActive = false; + this.upcomingActive = false; + this.movies = await this.searchService.popularMoviesByPage(0, 12); + this.tvShows = await this.searchService.popularTvByPage(0, 12); + + this.createModel(); + this.scrollDisabled = false; + } + + public async trending() { + this.clear(); + + this.scrollDisabled = true; + this.isScrolling = false; + this.contentLoaded = 12; + this.loading() + this.popularActive = false; + this.trendingActive = true; + this.upcomingActive = false; + this.movies = await this.searchService.nowPlayingMoviesByPage(0, 12); + this.tvShows = await this.searchService.trendingTvByPage(0, 12); + + this.createModel(); + this.scrollDisabled = false; + } + + public async upcoming() { + this.clear(); + this.scrollDisabled = true; + this.isScrolling = false; + this.contentLoaded = 12; + this.loading() + this.popularActive = false; + this.trendingActive = false; + this.upcomingActive = true; + this.movies = await this.searchService.upcomingMoviesByPage(0, 12); + this.tvShows = await this.searchService.anticipatedTvByPage(0, 12); + + this.createModel(); + this.scrollDisabled = false; + } + + private createModel() { + const tempResults = []; + this.movies.forEach(m => { + tempResults.push({ + available: m.available, + posterPath: `https://image.tmdb.org/t/p/w300/${m.posterPath}`, + requested: m.requested, + title: m.title, + type: RequestType.movie, + id: m.id, + url: `http://www.imdb.com/title/${m.imdbId}/`, + rating: m.voteAverage, + overview: m.overview, + approved: m.approved + }); + }); + this.tvShows.forEach(m => { + tempResults.push({ + available: m.available, + posterPath: "../../../images/default_tv_poster.png", + requested: m.requested, + title: m.title, + type: RequestType.tvShow, + id: m.id, + url: undefined, + rating: +m.rating, + overview: m.overview, + approved: m.approved + }); + }); + this.shuffle(tempResults); + this.discoverResults.push(...tempResults); + + this.finishLoading(); + } + + private createInitialModel() { this.movies.forEach(m => { this.discoverResults.push({ available: m.available, @@ -41,7 +172,8 @@ export class DiscoverComponent implements OnInit { id: m.id, url: `http://www.imdb.com/title/${m.imdbId}/`, rating: m.voteAverage, - overview: m.overview + overview: m.overview, + approved: m.approved }); }); this.tvShows.forEach(m => { @@ -54,18 +186,31 @@ export class DiscoverComponent implements OnInit { id: m.id, url: undefined, rating: +m.rating, - overview: m.overview + overview: m.overview, + approved: m.approved }); }); - this.shuffle(this.discoverResults); + this.finishLoading(); } - private shuffle(discover: IDiscoverCardResult[]) : IDiscoverCardResult[] { + private shuffle(discover: IDiscoverCardResult[]): IDiscoverCardResult[] { for (let i = discover.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [discover[i], discover[j]] = [discover[j], discover[i]]; } return discover; } + + private loading() { + this.loadingFlag = true; + } + + private clear() { + this.discoverResults = []; + } + + private finishLoading() { + this.loadingFlag = false; + } } diff --git a/src/Ombi/ClientApp/src/app/discover/discover.module.ts b/src/Ombi/ClientApp/src/app/discover/discover.module.ts index 6a1818fb5..cb40f9074 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.module.ts +++ b/src/Ombi/ClientApp/src/app/discover/discover.module.ts @@ -1,7 +1,8 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { SearchService } from "../services"; +import { SearchService, RequestService } from "../services"; import { SharedModule } from "../shared/shared.module"; import { DiscoverComponent } from "./discover.component"; @@ -10,20 +11,24 @@ import { AuthGuard } from "../auth/auth.guard"; import { PipeModule } from "../pipes/pipe.module"; import { DiscoverCardDetailsComponent } from "./card/discover-card-details.component"; import { MatDialog } from "@angular/material"; +import { DiscoverCollectionsComponent } from "./collections/discover-collections.component"; const routes: Routes = [ { path: "", component: DiscoverComponent, canActivate: [AuthGuard] }, + { path: "collection/:collectionId", component: DiscoverCollectionsComponent, canActivate: [AuthGuard] } ]; @NgModule({ imports: [ RouterModule.forChild(routes), SharedModule, PipeModule, + InfiniteScrollModule, ], declarations: [ DiscoverComponent, DiscoverCardComponent, DiscoverCardDetailsComponent, + DiscoverCollectionsComponent, ], entryComponents: [ DiscoverCardDetailsComponent @@ -34,6 +39,7 @@ const routes: Routes = [ providers: [ SearchService, MatDialog, + RequestService, ], }) diff --git a/src/Ombi/ClientApp/src/app/discover/interfaces.ts b/src/Ombi/ClientApp/src/app/discover/interfaces.ts index e2e5e7e67..808e7b6ae 100644 --- a/src/Ombi/ClientApp/src/app/discover/interfaces.ts +++ b/src/Ombi/ClientApp/src/app/discover/interfaces.ts @@ -7,6 +7,7 @@ export interface IDiscoverCardResult { title: string; type: RequestType; available: boolean; + approved: boolean; requested: boolean; rating: number; overview: string; diff --git a/src/Ombi/ClientApp/src/app/interfaces/ICalendar.ts b/src/Ombi/ClientApp/src/app/interfaces/ICalendar.ts new file mode 100644 index 000000000..0c81f1513 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/interfaces/ICalendar.ts @@ -0,0 +1,5 @@ +export interface ICalendarModel { + title: string; + startDate: Date; + endDate: Date; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/ICommon.ts b/src/Ombi/ClientApp/src/app/interfaces/ICommon.ts index c2e1e7e1d..06f33eb69 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ICommon.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ICommon.ts @@ -26,3 +26,10 @@ export interface IUsersModel { id: string; username: string; } + +export interface INavBar { + icon: string; + name: string; + link: string; + requiresAdmin: boolean; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts b/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts index f368ee035..5472a6c7c 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts @@ -93,6 +93,14 @@ export interface IPushoverNotificationSettings extends INotificationSettings { sound: string; } +export interface IGotifyNotificationSettings extends INotificationSettings { + accessToken: string; + notificationTemplates: INotificationTemplates[]; + baseUrl: string; + applicationToken: string; + priority: number; +} + export interface IMattermostNotifcationSettings extends INotificationSettings { webhookUrl: string; username: string; diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts b/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts index b643993f4..9370cc6e7 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts @@ -17,3 +17,13 @@ export interface IMinimumAvailability { value: string; name: string; } + +export interface IAdvancedModel { + profile: IRadarrProfile; + rootFolder: IRadarrRootFolder; +} + +export interface IAdvancedData { + profiles: IRadarrProfile[]; + rootFolders: IRadarrRootFolder[]; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts index e9b7c53f7..a45871770 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts @@ -114,6 +114,8 @@ export interface ITvRequests { export interface IChildRequests extends IBaseRequest { seasonRequests: INewSeasonRequests[]; + parentRequestId: number; + parentRequest: ITvRequests; subscribed: boolean; showSubscribe: boolean; } @@ -139,6 +141,7 @@ export interface INewSeasonRequests { id: number; seasonNumber: number; episodes: IEpisodesRequests[]; + seasonAvailable: boolean; } export interface IEpisodesRequests { @@ -146,6 +149,7 @@ export interface IEpisodesRequests { episodeNumber: number; title: string; airDate: Date; + airDateDisplay: string; url: string; available: boolean; requested: boolean; diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts b/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts index b13ed3abd..5787a363a 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts @@ -39,6 +39,8 @@ subscribed: boolean; showSubscribe: boolean; externalIds: IExternalIds; + keywords: IKeywords; + belongsToCollection: ICollectionsModel; // for the UI requestProcessing: boolean; @@ -46,6 +48,23 @@ background: any; } + +export interface ICollectionsModel { + id: number; + name: string; + posterPath: string; + backdropPath: string; +} + +export interface IKeywords { + keywordsValue: IKeywordsValue[]; +} + +export interface IKeywordsValue { + id: number; + name: string; +} + export interface IVideos { results: IVideoResult[]; } diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts b/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts new file mode 100644 index 000000000..586f638e0 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts @@ -0,0 +1,109 @@ +import { INewSeasonRequests, RequestType } from "./IRequestModel"; + +export interface ISearchTvResultV2 { + id: number; + title: string; // used in the request + aliases: string[]; + background: any; + banner: string; + seriesId: number; + status: string; + firstAired: string; + networkId: string; + runtime: string; + genre: string[]; + overview: string; + lastUpdated: number; + airsDayOfWeek: string; + airsTime: string; + rating: string; + imdbId: string; + siteRating: number; + trailer: string; + homepage: string; + certification: string; + seasonRequests: INewSeasonRequests[]; + requestAll: boolean; + approved: boolean; + requested: boolean; + available: boolean; + plexUrl: string; + embyUrl: string; + quality: string; + firstSeason: boolean; + latestSeason: boolean; + theTvDbId: string; + subscribed: boolean; + showSubscribe: boolean; + fullyAvailable: boolean; + partlyAvailable: boolean; + network: INetwork; + images: IImagesV2; + cast: ICast[]; + crew: ICrew[]; + requestId: number; +} + +export interface IMovieCollectionsViewModel { + name: string; + overview: string; + collection: IMovieCollection[]; +} + +export interface IMovieCollection { + id: number; + overview: string; + posterPath: string; + title: string; + type: RequestType; + + approved: boolean; + requested: boolean; + available: boolean; + plexUrl: string; + embyUrl: string; + imdbId: string; +} + +export interface INetwork { + id: number; + name: string; + country: ICountry; +} + +export interface ICountry { + name: string; + code: string; + timezone: string; +} + +export interface IImagesV2 { + medium: string; + original: string; +} + +export interface ICast { + self: boolean; + voide: boolean; + person: IPersonViewModel; + character: ICharacterViewModel; +} + +export interface IPersonViewModel { + id: number; + url: string; + name: string; + image: IImagesV2; +} + +export interface ICharacterViewModel { + id: number; + url: string; + name: string; + image: IImagesV2; +} + +export interface ICrew { + type: string; + person: IPersonViewModel; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts index a44750040..0816ad42e 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts @@ -21,7 +21,6 @@ export interface IUser { episodeRequestQuota: IRemainingRequests | null; movieRequestQuota: IRemainingRequests | null; musicRequestQuota: IRemainingRequests | null; - checked: boolean; } export interface IUserQualityProfiles { diff --git a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.ts b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.ts index 1fda9acc5..1e30f932c 100644 --- a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, OnInit, Inject } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { AuthService } from "../auth/auth.service"; @@ -37,7 +37,7 @@ export class IssueDetailsComponent implements OnInit { private notificationService: NotificationService, private imageService: ImageService, private sanitizer: DomSanitizer, - private readonly platformLocation: PlatformLocation) { + @Inject(APP_BASE_HREF) href:string) { this.route.params .subscribe((params: any) => { this.issueId = parseInt(params.id); @@ -46,7 +46,7 @@ export class IssueDetailsComponent implements OnInit { this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser"); this.settingsService.getIssueSettings().subscribe(x => this.settings = x); - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = href; if (base) { this.defaultPoster = "../../.." + base + "/images/"; } else { diff --git a/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.ts b/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.ts index dc329baf3..5ccd0011a 100644 --- a/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.ts +++ b/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, OnDestroy, OnInit, Inject } from "@angular/core"; import { IMediaServerStatus } from "../interfaces"; import { ICustomizationSettings, ILandingPageSettings } from "../interfaces"; @@ -25,9 +25,11 @@ export class LandingPageComponent implements OnDestroy, OnInit { public baseUrl: string; private timer: any; + private href: string; + constructor(private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer, private landingPageService: LandingPageService, - private location: PlatformLocation) { } + @Inject(APP_BASE_HREF) href :string) { this.href = href } public ngOnInit() { this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); @@ -39,7 +41,7 @@ export class LandingPageComponent implements OnDestroy, OnInit { this.cycleBackground(); }, 15000); - const base = this.location.getBaseHrefFromDOM(); + const base = this.href; if (base.length > 1) { this.baseUrl = base; } diff --git a/src/Ombi/ClientApp/src/app/login/login.component.html b/src/Ombi/ClientApp/src/app/login/login.component.html index 097f0e85d..5742bcc9d 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.html +++ b/src/Ombi/ClientApp/src/app/login/login.component.html @@ -1,60 +1,84 @@ -
+ +
+
+
- -
-
- -
- -
-
- -
-

- -
- - - - - - -
- -
- - -
+ + +
+
- +
--> +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/login/login.component.scss b/src/Ombi/ClientApp/src/app/login/login.component.scss index a194b031b..fa2b609f4 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.scss +++ b/src/Ombi/ClientApp/src/app/login/login.component.scss @@ -44,12 +44,7 @@ div.bg { width: 100%; } -::ng-deep input.mat-input-element { - color: white; // choose the color you want - } -::ng-deep label.mat-input-placeholder { - color: #fff; // choose the color you want - } + .white-checkbox { color:white; } @@ -211,4 +206,14 @@ div.bg { .full-width { width: 100%; +} + +.small-middle-container{ + margin: auto; + width: 25%; + overflow: auto; +} + +.top-margin { + margin-top:10%; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/login/login.component.ts b/src/Ombi/ClientApp/src/app/login/login.component.ts index 03aafe20b..2bf21701e 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.ts +++ b/src/Ombi/ClientApp/src/app/login/login.component.ts @@ -1,9 +1,9 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit, Inject } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { AuthService } from "../auth/auth.service"; import { IAuthenticationSettings, ICustomizationSettings } from "../interfaces"; import { NotificationService, PlexTvService } from "../services"; @@ -14,6 +14,7 @@ import { DomSanitizer } from "@angular/platform-browser"; import { ImageService } from "../services"; import { fadeInOutAnimation } from "../animations/fadeinout"; +import { StorageService } from "../shared/storage/storage-service"; @Component({ templateUrl: "./login.component.html", @@ -45,11 +46,14 @@ export class LoginComponent implements OnDestroy, OnInit { private errorBody: string; private errorValidation: string; + private href: string; constructor(private authService: AuthService, private router: Router, private notify: NotificationService, private status: StatusService, private fb: FormBuilder, private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer, - private route: ActivatedRoute, private location: PlatformLocation, private translate: TranslateService, private plexTv: PlexTvService) { - this.route.params + private route: ActivatedRoute, @Inject(APP_BASE_HREF) href:string, private translate: TranslateService, private plexTv: PlexTvService, + private store: StorageService) { + this.href = href; + this.route.params .subscribe((params: any) => { this.landingFlag = params.landing; if (!this.landingFlag) { @@ -89,7 +93,7 @@ export class LoginComponent implements OnDestroy, OnInit { this.cycleBackground(); }, 15000); - const base = this.location.getBaseHrefFromDOM(); + const base = this.href; if (base.length > 1) { this.baseUrl = base; } @@ -113,7 +117,7 @@ export class LoginComponent implements OnDestroy, OnInit { } this.authService.login(user) .subscribe(x => { - localStorage.setItem("id_token", x.access_token); + this.store.save("id_token", x.access_token); if (this.authService.loggedIn()) { this.ngOnDestroy(); @@ -151,7 +155,7 @@ export class LoginComponent implements OnDestroy, OnInit { public getPinResult(pinId: number) { this.authService.oAuth(pinId).subscribe(x => { if(x.access_token) { - localStorage.setItem("id_token", x.access_token); + this.store.save("id_token", x.access_token); if (this.authService.loggedIn()) { this.ngOnDestroy(); diff --git a/src/Ombi/ClientApp/src/app/login/loginoauth.component.ts b/src/Ombi/ClientApp/src/app/login/loginoauth.component.ts index 18f90be5f..0671fd7c3 100644 --- a/src/Ombi/ClientApp/src/app/login/loginoauth.component.ts +++ b/src/Ombi/ClientApp/src/app/login/loginoauth.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { AuthService } from "../auth/auth.service"; import { NotificationService } from "../services"; +import { StorageService } from "../shared/storage/storage-service"; @Component({ templateUrl: "./loginoauth.component.html", @@ -12,7 +13,8 @@ export class LoginOAuthComponent implements OnInit { public error: string; constructor(private authService: AuthService, private router: Router, - private route: ActivatedRoute, private notify: NotificationService) { + private route: ActivatedRoute, private notify: NotificationService, + private store: StorageService) { this.route.params .subscribe((params: any) => { this.pin = params.pin; @@ -26,7 +28,7 @@ export class LoginOAuthComponent implements OnInit { public auth() { this.authService.oAuth(this.pin).subscribe(x => { if (x.access_token) { - localStorage.setItem("id_token", x.access_token); + this.store.save("id_token", x.access_token); if (this.authService.loggedIn()) { this.router.navigate(["search"]); diff --git a/src/Ombi/ClientApp/src/app/login/resetpassword.component.ts b/src/Ombi/ClientApp/src/app/login/resetpassword.component.ts index 2203b95b9..7c228e962 100644 --- a/src/Ombi/ClientApp/src/app/login/resetpassword.component.ts +++ b/src/Ombi/ClientApp/src/app/login/resetpassword.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, OnInit, Inject } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { DomSanitizer } from "@angular/platform-browser"; @@ -17,10 +17,12 @@ export class ResetPasswordComponent implements OnInit { public emailSettingsEnabled: boolean; public baseUrl: string; public background: any; + private href: string; constructor(private identityService: IdentityService, private notify: NotificationService, - private fb: FormBuilder, private settingsService: SettingsService, private location: PlatformLocation, + private fb: FormBuilder, private settingsService: SettingsService, @Inject(APP_BASE_HREF) href:string, private images: ImageService, private sanitizer: DomSanitizer) { + this.href = href; this.form = this.fb.group({ email: ["", [Validators.required]], }); @@ -30,7 +32,7 @@ export class ResetPasswordComponent implements OnInit { this.images.getRandomBackground().subscribe(x => { this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%),url(" + x.url + ")"); }); - const base = this.location.getBaseHrefFromDOM(); + const base = this.href; if (base.length > 1) { this.baseUrl = base; } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/index.ts b/src/Ombi/ClientApp/src/app/media-details/components/index.ts new file mode 100644 index 000000000..aae142fa1 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/index.ts @@ -0,0 +1,37 @@ +import { MovieDetailsComponent } from "./movie/movie-details.component"; +import { YoutubeTrailerComponent } from "./shared/youtube-trailer.component"; +import { TvDetailsComponent } from "./tv/tv-details.component"; +import { MovieInformationPanelComponent } from "./movie/panels/movie-information-panel.component"; +import { TvInformationPanelComponent } from "./tv/panels/tv-information-panel/tv-information-panel.component"; +import { TopBannerComponent } from "./shared/top-banner/top-banner.component"; +import { SocialIconsComponent } from "./shared/social-icons/social-icons.component"; +import { MediaPosterComponent } from "./shared/media-poster/media-poster.component"; +import { CastCarouselComponent } from "./shared/cast-carousel/cast-carousel.component"; +import { DenyDialogComponent } from "./shared/deny-dialog/deny-dialog.component"; +import { TvRequestsPanelComponent } from "./tv/panels/tv-requests/tv-requests-panel.component"; +import { MovieAdminPanelComponent } from "./movie/panels/movie-admin-panel/movie-admin-panel.component"; +import { MovieAdvancedOptionsComponent } from "./movie/panels/movie-advanced-options/movie-advanced-options.component"; + +export const components: any[] = [ + MovieDetailsComponent, + YoutubeTrailerComponent, + TvDetailsComponent, + MovieInformationPanelComponent, + TvInformationPanelComponent, + TopBannerComponent, + SocialIconsComponent, + MediaPosterComponent, + CastCarouselComponent, + DenyDialogComponent, + TvRequestsPanelComponent, + MovieAdminPanelComponent, + MovieAdvancedOptionsComponent +]; + + + +export const entryComponents: any[] = [ + YoutubeTrailerComponent, + DenyDialogComponent, + MovieAdvancedOptionsComponent, +]; diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html new file mode 100644 index 000000000..e59b474ad --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html @@ -0,0 +1,208 @@ +
+ +
+ +
+ + + +
+
+ +
+ + + + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+ +
+
+
+ + + {{movie.overview}} + + +
+
+ +
+
+ +
+
+ +
+
+ + + + + {{'MediaDetails.RecommendationsTitle' | translate}} + + + +
+ +
+ + +
+
+
+ + + + {{'MediaDetails.SimilarTitle' | translate}} + + + +
+ +
+ + +
+
+
+ + + + {{'MediaDetails.VideosTitle' | translate}} + + + +
+ +
+ +
+
+
+
+
+
+ + + +
+
+ + + + +
+ + + + +
+
+
+ + + + + + + + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts new file mode 100644 index 000000000..b8a6c3e7b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts @@ -0,0 +1,103 @@ +import { Component, ViewEncapsulation } from "@angular/core"; +import { ImageService, SearchV2Service, RequestService, MessageService } from "../../../services"; +import { ActivatedRoute } from "@angular/router"; +import { DomSanitizer } from "@angular/platform-browser"; +import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; +import { MatDialog } from "@angular/material"; +import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component"; +import { AuthService } from "../../../auth/auth.service"; +import { IMovieRequests, RequestType } from "../../../interfaces"; +import { DenyDialogComponent } from "../shared/deny-dialog/deny-dialog.component"; + +@Component({ + templateUrl: "./movie-details.component.html", + styleUrls: ["../../media-details.component.scss"], + encapsulation: ViewEncapsulation.None +}) +export class MovieDetailsComponent { + public movie: ISearchMovieResultV2; + public hasRequest: boolean; + public movieRequest: IMovieRequests; + public isAdmin: boolean; + + private theMovidDbId: number; + + constructor(private searchService: SearchV2Service, private route: ActivatedRoute, + private sanitizer: DomSanitizer, private imageService: ImageService, + public dialog: MatDialog, private requestService: RequestService, + public messageService: MessageService, private auth: AuthService) { + this.route.params.subscribe((params: any) => { + this.theMovidDbId = params.movieDbId; + this.load(); + }); + } + + public load() { + + this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(async x => { + this.movie = x; + if (this.movie.requestId > 0) { + // Load up this request + this.hasRequest = true; + this.movieRequest = await this.requestService.getMovieRequest(this.movie.requestId); + } + this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => { + this.movie.background = this.sanitizer.bypassSecurityTrustStyle + ("url(" + x + ")"); + }); + }); + + } + + public async request() { + const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null }).toPromise(); + if (result.result) { + this.movie.requested = true; + this.messageService.send(result.message, "Ok"); + } else { + this.messageService.send(result.errorMessage, "Ok"); + } + } + + public openDialog() { + this.dialog.open(YoutubeTrailerComponent, { + width: '560px', + data: this.movie.videos.results[0].key + }); + } + + public async deny() { + const dialogRef = this.dialog.open(DenyDialogComponent, { + width: '250px', + data: {requestId: this.movieRequest.id, requestType: RequestType.movie} + }); + + dialogRef.afterClosed().subscribe(result => { + this.movieRequest.denied = result; + if(this.movieRequest.denied) { + this.movie.approved = false; + } + }); + } + + public async approve() { + const result = await this.requestService.approveMovie({ id: this.movieRequest.id }).toPromise(); + if (result.result) { + this.movie.approved = false; + this.messageService.send("Successfully Approved", "Ok"); + } else { + this.messageService.send(result.errorMessage, "Ok"); + } + } + + public async markAvailable() { + const result = await this.requestService.markMovieAvailable({id: this.movieRequest.id}).toPromise(); + if (result.result) { + this.movie.available = true; + this.messageService.send(result.message, "Ok"); + } else { + this.messageService.send(result.errorMessage, "Ok"); + } + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html new file mode 100644 index 000000000..7d20cfc08 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.ts new file mode 100644 index 000000000..a67390364 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.ts @@ -0,0 +1,63 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { RadarrService } from "../../../../../services"; +import { IRadarrProfile, IRadarrRootFolder, IMovieRequests, IAdvancedData } from "../../../../../interfaces"; +import { MatDialog } from "@angular/material"; +import { MovieAdvancedOptionsComponent } from "../movie-advanced-options/movie-advanced-options.component"; + +@Component({ + templateUrl: "./movie-admin-panel.component.html", + selector: "movie-admin-panel", +}) +export class MovieAdminPanelComponent implements OnInit { + + @Input() public movie: IMovieRequests; + + public radarrProfiles: IRadarrProfile[]; + public selectedRadarrProfile: IRadarrProfile; + public radarrRootFolders: IRadarrRootFolder[]; + public selectRadarrRootFolders: IRadarrRootFolder; + + constructor(private radarrService: RadarrService, private dialog: MatDialog) { } + + public async ngOnInit() { + if (await this.radarrService.isRadarrEnabled()) { + this.radarrService.getQualityProfilesFromSettings().subscribe(c => { + this.radarrProfiles = c; + this.setQualityOverrides(); + }); + this.radarrService.getRootFoldersFromSettings().subscribe(c => { + this.radarrRootFolders = c; + this.setRootFolderOverrides(); + }); + } + } + + public openAdvancedOptions() { + const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: { profiles: this.radarrProfiles, rootFolders: this.radarrRootFolders }, panelClass: 'modal-panel' }) + dialog.afterClosed().subscribe(result => { + console.log(result); + }); + } + + private setQualityOverrides(): void { + if (this.radarrProfiles) { + const profile = this.radarrProfiles.filter((p) => { + return p.id === this.movie.qualityOverride; + }); + if (profile.length > 0) { + this.movie.qualityOverrideTitle = profile[0].name; + } + } + } + + private setRootFolderOverrides(): void { + if (this.radarrRootFolders) { + const path = this.radarrRootFolders.filter((folder) => { + return folder.id === this.movie.rootPathOverride; + }); + if (path.length > 0) { + this.movie.rootPathOverrideTitle = path[0].path; + } + } + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-advanced-options/movie-advanced-options.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-advanced-options/movie-advanced-options.component.html new file mode 100644 index 000000000..76cc80869 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-advanced-options/movie-advanced-options.component.html @@ -0,0 +1,21 @@ + + +

Advanced Options

+
+ + Radarr Quality Profile + + {{profile.name}} + + + + + Radarr Root Folders + + {{profile.path}} + + +
+
+ +
diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-advanced-options/movie-advanced-options.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-advanced-options/movie-advanced-options.component.ts new file mode 100644 index 000000000..93d0d7061 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-advanced-options/movie-advanced-options.component.ts @@ -0,0 +1,14 @@ +import { Component, Inject } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; +import { IAdvancedModel, IAdvancedData } from "../../../../../interfaces"; + +@Component({ + templateUrl: "./movie-advanced-options.component.html", + selector: "movie-advanced-options", +}) +export class MovieAdvancedOptionsComponent { + + public options: IAdvancedModel; + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData) { } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html new file mode 100644 index 000000000..359588028 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html @@ -0,0 +1,80 @@ +
+
+ Status: +
{{movie.status}}
+
+
+ Availability +
{{'Common.Available' | translate}}
+
{{'Common.NotAvailable' | translate}}
+ +
+ +
+ Request Status +
{{'Common.ProcessingRequest' | translate}}
+
{{'Common.PendingApproval' | translate}} +
+
{{'Common.NotRequested' | translate}} +
+
+ +
+
+ Genres: +
+ + + {{genre.name}} + + +
+
+
+ + Theatrical Release: +
+ + {{movie.releaseDate | date: 'mediumDate'}} +
+ Digital Release: +
+ {{movie.digitalReleaseDate | date: 'mediumDate'}} +
+
+
+ User Score: +
+ {{movie.voteAverage | number:'1.0-1'}} / 10 +
+
+
+ Votes: +
+ {{movie.voteCount | thousandShort: 1}} +
+
+
+ Runtime: +
{{movie.runtime}} Minutes
+
+
+ Revenue: +
{{movie.revenue | currency: 'USD'}}
+
+
+ Budget: +
{{movie.budget | currency: 'USD'}}
+
+ + +
+
+ Keywords/Tags: + + + {{keyword.name}} + + +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.ts new file mode 100644 index 000000000..5d5d9005e --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.ts @@ -0,0 +1,12 @@ +import { Component, ViewEncapsulation, Input } from "@angular/core"; +import { ISearchMovieResultV2 } from "../../../../interfaces/ISearchMovieResultV2"; + +@Component({ + templateUrl: "./movie-information-panel.component.html", + styleUrls: ["../../../media-details.component.scss"], + selector: "movie-information-panel", + encapsulation: ViewEncapsulation.None +}) +export class MovieInformationPanelComponent { + @Input() public movie: ISearchMovieResultV2; +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.html new file mode 100644 index 000000000..5a6982efd --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.html @@ -0,0 +1,24 @@ + + {{'MediaDetails.Casts.CastTitle' | translate}} + + + +
+
+ + + +
+
+ {{'MediaDetails.Casts.Character' | translate}}: {{item.character}} + {{'MediaDetails.Casts.Character' | translate}}: {{item.character.name}} +
+
+ {{'MediaDetails.Casts.Actor' | translate}}: {{item.name}} + {{'MediaDetails.Casts.Actor' | translate}}: {{item.person.name}} +
+
+
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.scss b/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.scss new file mode 100644 index 000000000..d9db92e00 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.scss @@ -0,0 +1,7 @@ +@import "~@angular/material/theming"; +@import "~styles/variables.scss"; +.actor-background { + .dark & { + background: $backgroundTint-dark; + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.ts new file mode 100644 index 000000000..d7baa81f0 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/cast-carousel/cast-carousel.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "cast-carousel", + templateUrl: "./cast-carousel.component.html", + styleUrls: ["./cast-carousel.component.scss"] +}) +export class CastCarouselComponent { + + @Input() cast: any[]; +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/deny-dialog/deny-dialog.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/deny-dialog/deny-dialog.component.html new file mode 100644 index 000000000..5863b76eb --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/deny-dialog/deny-dialog.component.html @@ -0,0 +1,10 @@ +

{{ 'Requests.DenyReason' | translate}}

+
+ + + +
+
+ + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/deny-dialog/deny-dialog.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/deny-dialog/deny-dialog.component.ts new file mode 100644 index 000000000..8d6e92e64 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/deny-dialog/deny-dialog.component.ts @@ -0,0 +1,47 @@ +import { Component, Inject, Output, EventEmitter } from "@angular/core"; +import { IDenyDialogData } from "../interfaces/interfaces"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; +import { RequestService, MessageService } from "../../../../services"; +import { RequestType, IRequestEngineResult } from "../../../../interfaces"; + +@Component({ + selector: "deny-dialog", + templateUrl: "./deny-dialog.component.html", +}) +export class DenyDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: IDenyDialogData, + private requestService: RequestService, + public messageService: MessageService) {} + + public denyReason: string; + + public async deny() { + let result: IRequestEngineResult; + if(this.data.requestType == RequestType.movie) { + result = await this.requestService.denyMovie({id: this.data.requestId, reason: this.denyReason }).toPromise(); + } + if(this.data.requestType == RequestType.tvShow) { + result = await this.requestService.denyChild({id: this.data.requestId, reason: this.denyReason }).toPromise(); + } + if(this.data.requestType == RequestType.album) { + result = await this.requestService.denyAlbum({id: this.data.requestId, reason: this.denyReason }).toPromise(); + } + + if (result.result) { + this.messageService.send("Denied Request", "Ok"); + this.data.denied = true; + } else { + this.messageService.send(result.errorMessage, "Ok"); + this.data.denied = false; + } + + this.dialogRef.close(); + } + + onNoClick(): void { + this.dialogRef.close(); + this.data.denied = false; + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/interfaces/interfaces.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/interfaces/interfaces.ts new file mode 100644 index 000000000..205a146c4 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/interfaces/interfaces.ts @@ -0,0 +1,7 @@ +import { RequestType } from "../../../../interfaces"; + +export interface IDenyDialogData { + requestType: RequestType; + requestId: number; + denied: boolean; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/media-poster/media-poster.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/media-poster/media-poster.component.html new file mode 100644 index 000000000..74fd2a4c6 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/media-poster/media-poster.component.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/media-poster/media-poster.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/media-poster/media-poster.component.ts new file mode 100644 index 000000000..8e7a3b2b4 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/media-poster/media-poster.component.ts @@ -0,0 +1,10 @@ +import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; + +@Component({ + selector: "media-poster", + templateUrl: "./media-poster.component.html", +}) +export class MediaPosterComponent { + + @Input() posterPath: string; +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.html new file mode 100644 index 000000000..4dbbf416e --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.scss b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.scss new file mode 100644 index 000000000..1925e4e98 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.scss @@ -0,0 +1,8 @@ + +.grow-social { + transition: all .2s ease-in-out; +} +.grow-social:hover { + transform: scale(1.1); + color: black; +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.ts new file mode 100644 index 000000000..7b1e69ee3 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.ts @@ -0,0 +1,28 @@ +import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; + +@Component({ + selector: "social-icons", + templateUrl: "./social-icons.component.html", + styleUrls: ["./social-icons.component.scss"] +}) +export class SocialIconsComponent { + + @Input() homepage: string; + @Input() theMoviedbId: number; + @Input() hasTrailer: boolean; + @Input() imdbId: string; + @Input() tvdbId: string; + @Input() twitter: string; + @Input() facebook: string; + @Input() instagram: string; + @Input() available: boolean; + @Input() plexUrl: string; + @Input() embyUrl: string; + + @Output() openTrailer: EventEmitter = new EventEmitter(); + + + public openDialog() { + this.openTrailer.emit(); + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html new file mode 100644 index 000000000..d33fd7788 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html @@ -0,0 +1,20 @@ + +
+
+
+ +
+
+
+
+

{{title}} + ({{releaseDate | amLocal | amDateFormat: 'YYYY'}}) +

+ +
{{tagline}}
+
+
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts new file mode 100644 index 000000000..f833a197a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts @@ -0,0 +1,15 @@ +import { Component, Inject, Input } from "@angular/core"; + +@Component({ + selector: "top-banner", + templateUrl: "./top-banner.component.html", +}) +export class TopBannerComponent { + + @Input() title: string; + @Input() releaseDate: Date; + @Input() tagline: string; + @Input() available: boolean; + @Input() background: any; + +} diff --git a/src/Ombi/ClientApp/src/app/media-details/movie-details-trailer.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/youtube-trailer.component.html similarity index 72% rename from src/Ombi/ClientApp/src/app/media-details/movie-details-trailer.component.html rename to src/Ombi/ClientApp/src/app/media-details/components/shared/youtube-trailer.component.html index 925ebc906..630e64748 100644 --- a/src/Ombi/ClientApp/src/app/media-details/movie-details-trailer.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/youtube-trailer.component.html @@ -1,2 +1,2 @@ - \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/youtube-trailer.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/youtube-trailer.component.ts new file mode 100644 index 000000000..3456759cf --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/youtube-trailer.component.ts @@ -0,0 +1,14 @@ +import { Component, Inject } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; + +@Component({ + selector: "youtube-trailer", + templateUrl: "./youtube-trailer.component.html", +}) +export class YoutubeTrailerComponent { + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public youtubeLink: string) {} + +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html new file mode 100644 index 000000000..5c5844817 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html @@ -0,0 +1,61 @@ +
+
+ Status: +
+ {{tv.status}} +
+
+ First Aired: +
+ {{tv.firstAired | date: 'mediumDate'}} +
+
+ + +
+ Status: +
+ {{tv.status}} +
+
+
+ Runtime: +
+ {{tv.runtime}} Minutes +
+
+
+ Rating: +
+ {{tv.rating}} / 10 +
+
+
+ Network: +
+ {{tv.network.name}} +
+
+ +
+ Genres: +
+ + {{genre}} | + +
+
+ +
+
+ Seasons: +
+ {{seasonCount}} +
+
+
+ Episodes: +
+ {{totalEpisodes}} +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts new file mode 100644 index 000000000..0e64c566a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts @@ -0,0 +1,23 @@ +import { Component, ViewEncapsulation, Input, OnInit } from "@angular/core"; +import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2"; + +@Component({ + templateUrl: "./tv-information-panel.component.html", + styleUrls: ["../../../../media-details.component.scss"], + selector: "tv-information-panel", + encapsulation: ViewEncapsulation.None +}) +export class TvInformationPanelComponent implements OnInit { + @Input() public tv: ISearchTvResultV2; + + public seasonCount: number; + public totalEpisodes: number = 0; + public nextEpisode: any; + + public ngOnInit(): void { + this.tv.seasonRequests.forEach(season => { + this.totalEpisodes = this.totalEpisodes + season.episodes.length; + }); + this.seasonCount = this.tv.seasonRequests.length; + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.html new file mode 100644 index 000000000..661c11e86 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.html @@ -0,0 +1,79 @@ + + + + + +
{{'Common.ProcessingRequest' | translate}}
+
{{'Common.Denied' | translate}}
+
+ {{'Common.PendingApproval' | translate}} +
+
+ {{'Common.NotRequested' | translate}} +
+
{{'Common.Available' | translate}} +
+
+ + Requested By '{{request.requestedUser.userAlias}}' on + {{request.requestedDate | amLocal | amDateFormat: 'LL' }} + - {{request.deniedReason}} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'Requests.Number' | translate }} {{element.episodeNumber}} {{ 'Requests.GridTitle' | translate }} {{element.title}} {{ 'Requests.AirDate' | translate }} {{element.airDate | amLocal | amDateFormat: 'L' }} {{ 'Requests.GridStatus' | translate }} + + + + + +
+
+
+
+
+ +
+ +
+ + + + +
+ + + +
+ +
diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.scss b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.ts new file mode 100644 index 000000000..fe32ae988 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-requests/tv-requests-panel.component.ts @@ -0,0 +1,87 @@ +import { Component, Input } from "@angular/core"; +import { IChildRequests, RequestType } from "../../../../../interfaces"; +import { RequestService } from "../../../../../services/request.service"; +import { MessageService } from "../../../../../services"; +import { MatDialog } from "@angular/material"; +import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component"; + +@Component({ + templateUrl: "./tv-requests-panel.component.html", + styleUrls: ["./tv-requests-panel.component.scss"], + selector: "tv-requests-panel" +}) +export class TvRequestsPanelComponent { + @Input() public tvRequest: IChildRequests[]; + @Input() public isAdmin: boolean; + + public displayedColumns: string[] = ['number', 'title', 'airDate', 'status']; + + constructor(private requestService: RequestService, private messageService: MessageService, + public dialog: MatDialog) { + + } + + public async approve(request: IChildRequests) { + const result = await this.requestService.approveChild({ + id: request.id + }).toPromise(); + + if (result.result) { + request.approved = true; + request.denied = false; + request.seasonRequests.forEach((season) => { + season.episodes.forEach((ep) => { + ep.approved = true; + }); + }); + this.messageService.send("Request has been approved", "Ok"); + } else { + this.messageService.send(result.errorMessage, "Ok"); + } + } + + public changeAvailability(request: IChildRequests, available: boolean) { + request.available = available; + request.seasonRequests.forEach((season) => { + season.episodes.forEach((ep) => { + ep.available = available; + }); + }); + if (available) { + this.requestService.markTvAvailable({ id: request.id }).subscribe(x => { + if (x.result) { + this.messageService.send( + `This request is now available`); + } else { + this.messageService.send("Request Available", x.message ? x.message : x.errorMessage); + request.approved = false; + } + }); + } else { + this.requestService.markTvUnavailable({ id: request.id }).subscribe(x => { + if (x.result) { + this.messageService.send( + `This request is now unavailable`); + } else { + this.messageService.send("Request Available", x.message ? x.message : x.errorMessage); + request.approved = false; + } + }); + } + } + public async deny(request: IChildRequests) { + const dialogRef = this.dialog.open(DenyDialogComponent, { + width: '250px', + data: {requestId: request.id, requestType: RequestType.tvShow} + }); + + dialogRef.afterClosed().subscribe(result => { + request.denied = true; + request.seasonRequests.forEach((season) => { + season.episodes.forEach((ep) => { + ep.approved = false; + }); + }); + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html new file mode 100644 index 000000000..be653411a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html @@ -0,0 +1,102 @@ +
+ + + +
+
+
+ + + + +
+ + + + +
+ +
+ + + + + +
+
+ +
+
+ + + + + + + +
+ + +
+
+
+ + + {{tv.overview}} + + +
+
+ +
+ +
+
+
+ + +
+
+ + + +
+ + +
+ + + + + Requests + + + + + + + +
+ + +
+ + + +
+ + + + +
+
+
+ +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.ts new file mode 100644 index 000000000..438df619f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.ts @@ -0,0 +1,72 @@ +import { Component, ViewEncapsulation, OnInit } from "@angular/core"; +import { ImageService, SearchV2Service, MessageService, RequestService } from "../../../services"; +import { ActivatedRoute } from "@angular/router"; +import { DomSanitizer } from "@angular/platform-browser"; +import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; +import { MatDialog } from "@angular/material"; +import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component"; +import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; +import { IChildRequests } from "../../../interfaces"; +import { AuthService } from "../../../auth/auth.service"; + +@Component({ + templateUrl: "./tv-details.component.html", + styleUrls: ["../../media-details.component.scss"], + encapsulation: ViewEncapsulation.None +}) +export class TvDetailsComponent implements OnInit { + + public tv: ISearchTvResultV2; + public tvRequest: IChildRequests[]; + public fromSearch: boolean; + public isAdmin: boolean; + + private tvdbId: number; + + constructor(private searchService: SearchV2Service, private route: ActivatedRoute, + private sanitizer: DomSanitizer, private imageService: ImageService, + public dialog: MatDialog, public messageService: MessageService, private requestService: RequestService, + private auth: AuthService) { + this.route.params.subscribe((params: any) => { + this.tvdbId = params.tvdbId; + this.fromSearch = params.search; + }); + } + + public async ngOnInit() { + await this.load(); + } + + public async load() { + + this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + if (this.fromSearch) { + this.tv = await this.searchService.getTvInfoWithMovieDbId(this.tvdbId); + this.tvdbId = this.tv.id; + } else { + this.tv = await this.searchService.getTvInfo(this.tvdbId); + } + + if (this.tv.requestId) { + this.tvRequest = await this.requestService.getChildRequests(this.tv.requestId).toPromise(); + } + + const tvBanner = await this.imageService.getTvBanner(this.tvdbId).toPromise(); + this.tv.background = this.sanitizer.bypassSecurityTrustStyle("url(" + tvBanner + ")"); + } + + public async request() { + this.dialog.open(EpisodeRequestComponent, { width: "800px", data: this.tv, panelClass: 'modal-panel' }) + } + + public openDialog() { + debugger; + let trailerLink = this.tv.trailer; + trailerLink = trailerLink.split('?v=')[1]; + + this.dialog.open(YoutubeTrailerComponent, { + width: '560px', + data: trailerLink + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/media-details/movie-details.component.scss b/src/Ombi/ClientApp/src/app/media-details/media-details.component.scss similarity index 80% rename from src/Ombi/ClientApp/src/app/media-details/movie-details.component.scss rename to src/Ombi/ClientApp/src/app/media-details/media-details.component.scss index b1915941f..11a039a62 100644 --- a/src/Ombi/ClientApp/src/app/media-details/movie-details.component.scss +++ b/src/Ombi/ClientApp/src/app/media-details/media-details.component.scss @@ -1,4 +1,5 @@ -$ombi-primary:#3f3f3f; +@import "~@angular/material/theming"; +@import "~styles/variables.scss"; //MINE @media (max-width: 570px) { h1 { @@ -32,7 +33,7 @@ $ombi-primary:#3f3f3f; background-size: cover; background-position: 50% 10%; transition: all .5s; - height: 550px; + height: 450px; color: #fff; position: relative; } @@ -48,11 +49,10 @@ $ombi-primary:#3f3f3f; #summary-wrapper .shadow-base, .summary-wrapper .shadow-base { - height: 120px; bottom: 0; - background-image: -webkit-linear-gradient(top, transparent 0%, rgba(0, 0, 0, 0.8) 100%); - background-image: -o-linear-gradient(top, transparent 0%, rgba(0, 0, 0, 0.8) 100%); - background-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.8) 100%); + background-image: -webkit-linear-gradient(top, transparent 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -o-linear-gradient(top, transparent 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.5) 100%); background-repeat: repeat-x; } @@ -69,6 +69,10 @@ $ombi-primary:#3f3f3f; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#4D000000', GradientType=0); } +.available-bottom-border { + border-bottom: solid 8px #1DE9B6; +} + #summary-wrapper .summary .container, .summary-wrapper .summary .container { position: absolute; @@ -96,7 +100,7 @@ $ombi-primary:#3f3f3f; #summary-wrapper .summary .container h1, .summary-wrapper .summary .container h1 { margin: 0; - text-shadow: 0 0 20px #000; + text-shadow: 1px 1px 5px #000; line-height: 1.2; } @@ -104,16 +108,14 @@ $ombi-primary:#3f3f3f; min-height: 600px; } -section { - background-color: #fff; -} #info-wrapper .sidebar.affixable.affix-top { position: relative !important; } #info-wrapper .sidebar-poster { - margin-top: -180px; + margin-top: -280px; + width: 250px; } #info-wrapper .sidebar .poster { @@ -127,6 +129,10 @@ section { width: 100%; } +.full-width { + width: 100%; +} + #info-wrapper .sidebar .poster .real { height: 100%; top: 0; @@ -147,30 +153,50 @@ section { } .btn-spacing { - margin-top:10px; + margin-right:10px !important; +} + +.spacing-below { + margin-bottom: 15px !important; +} + +.left-seperator { + margin-left:40px; } .tagline { margin-top: 10px; margin-left: 10px; + text-shadow: 1px 1px 5px #000; } .preview-poster { width: 173px; } -.grow { - transition: all .2s ease-in-out; -} -.grow:hover { - transform: scale(1.1); -} - .media-icons { - color:$ombi-primary; + color: mat-color($ombi-app-primary) !important; padding: 1%; } .media-row { padding-top: 2%; +} + +.cast-profile-img { + border-radius: 10%; + width: 170px; +} + +.small-middle-container{ + margin: auto; + width: 95%; +} + +.keywords-panel { + margin-top: 8%; +} + +.medium-font { + font-size:16px; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/media-details.module.ts b/src/Ombi/ClientApp/src/app/media-details/media-details.module.ts index 659cebcef..da68ba6b2 100644 --- a/src/Ombi/ClientApp/src/app/media-details/media-details.module.ts +++ b/src/Ombi/ClientApp/src/app/media-details/media-details.module.ts @@ -1,34 +1,44 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { SearchService } from "../services"; +import { SearchService, RequestService, RadarrService } from "../services"; + +import {CarouselModule} from 'primeng/carousel'; import { SharedModule } from "../shared/shared.module"; -import { MovieDetailsComponent } from "./movie-details.component"; +import { MovieDetailsComponent } from "./components/movie/movie-details.component"; +import { TvDetailsComponent } from "./components/tv/tv-details.component"; import { PipeModule } from "../pipes/pipe.module"; -import { MovieDetailsTrailerComponent } from "./movie-details-trailer.component"; + +import * as fromComponents from './components'; +import { AuthGuard } from "../auth/auth.guard"; + const routes: Routes = [ - { path: "movie/:movieDbId", component: MovieDetailsComponent }, + { path: "movie/:movieDbId", component: MovieDetailsComponent, canActivate: [AuthGuard] }, + { path: "tv/:tvdbId/:search", component: TvDetailsComponent, canActivate: [AuthGuard] }, + { path: "tv/:tvdbId", component: TvDetailsComponent, canActivate: [AuthGuard] }, ]; @NgModule({ imports: [ RouterModule.forChild(routes), SharedModule, PipeModule, + CarouselModule, ], declarations: [ - MovieDetailsComponent, - MovieDetailsTrailerComponent + ...fromComponents.components ], exports: [ RouterModule, ], entryComponents: [ - MovieDetailsTrailerComponent + ...fromComponents.entryComponents ], providers: [ - SearchService + SearchService, + RequestService, + RadarrService, ], }) diff --git a/src/Ombi/ClientApp/src/app/media-details/movie-details-trailer.component.ts b/src/Ombi/ClientApp/src/app/media-details/movie-details-trailer.component.ts deleted file mode 100644 index bcebd3816..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/movie-details-trailer.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, Inject } from "@angular/core"; -import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; -import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2"; - -@Component({ - selector: "movie-trailer", - templateUrl: "./movie-details-trailer.component.html", -}) -export class MovieDetailsTrailerComponent { - - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: ISearchMovieResultV2) {} - -} diff --git a/src/Ombi/ClientApp/src/app/media-details/movie-details.component.html b/src/Ombi/ClientApp/src/app/media-details/movie-details.component.html deleted file mode 100644 index b0da3791f..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/movie-details.component.html +++ /dev/null @@ -1,237 +0,0 @@ -
- - -
-
-
- -
-
-
-
-

{{movie.title}} ({{movie.releaseDate - | date:'yyyy'}}) -

- -
{{movie.tagline}}
-
-
-
-
-
- -
-
-
- - - - -
- -
-
- - - -
- Theatrical Release: {{movie.releaseDate | date: 'mediumDate'}}
-
- Digital Release: - {{movie.digitalReleaseDate | date: - 'mediumDate'}} -
-
- User Score: {{movie.voteAverage | - number:'1.0-1'}}/10 -
-
- Votes: {{movie.voteCount | - thousandShort: 1}} -
-
- Status: {{movie.status}} -
-
- Runtime: {{movie.runtime}} Minutes -
-
- Revenue: {{movie.revenue | currency: - 'USD'}} -
-
- Budget: {{movie.budget | currency: 'USD'}} -
-
-
- - - -
-
- - - {{movie.overview}} - - -
-
-
-
-
- - - - - - -
-
- -
- -
-
-
-
- - -
- IMDb
- -
- -
-
- -
-
- - - - - Recommendations - - - -
- -
- - -
-
-
- - - - Similar - - - -
- -
- - -
-
-
- - - - Videos - - - -
- -
- -
-
-
-
-
-
- - - - -
-
- - - - - - - - - - - -
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/movie-details.component.ts b/src/Ombi/ClientApp/src/app/media-details/movie-details.component.ts deleted file mode 100644 index c8c5a3319..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/movie-details.component.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Component } from "@angular/core"; -import { ImageService, SearchV2Service } from "../services"; -import { ActivatedRoute } from "@angular/router"; -import { DomSanitizer } from "@angular/platform-browser"; -import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2"; -import { MatDialog } from "@angular/material"; -import { MovieDetailsTrailerComponent } from "./movie-details-trailer.component"; - -@Component({ - templateUrl: "./movie-details.component.html", - styleUrls: ["./movie-details.component.scss"], -}) -export class MovieDetailsComponent { - public movie: ISearchMovieResultV2; - private theMovidDbId: number; - - constructor(private searchService: SearchV2Service, private route: ActivatedRoute, - private sanitizer: DomSanitizer, private imageService: ImageService, - public dialog: MatDialog) { - this.route.params.subscribe((params: any) => { - this.theMovidDbId = params.movieDbId; - this.load(); - }); - } - - public load() { - this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(x => { - this.movie = x; - this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => { - this.movie.background = this.sanitizer.bypassSecurityTrustStyle - ("url(" + x + ")"); - }); - }); - - } - - public request() { - // - } - - public openDialog() { - this.dialog.open(MovieDetailsTrailerComponent, { - width: '560px', - data: this.movie - }); - } -} diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.css b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.css deleted file mode 100644 index c6eb0f3f3..000000000 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.css +++ /dev/null @@ -1,41 +0,0 @@ -.sidenav-container { - height: 100%; -} - -.sidenav { - width: 200px; -} - -.sidenav .mat-toolbar { - background: inherit; -} - -.mat-toolbar.mat-primary { - position: sticky; - top: 0; - z-index: 1; -} -.middle { - display: flex; -} - -.example-form { - min-width: 150px; - max-width: 500px; - width: 100%; -} - -.quater-width { - width: 25%; -} - -.autocomplete-img { - vertical-align: middle; - height: 63px; -} - -.mat-option { - height: 50px; - line-height: 50px; - padding: 0px 5px; -} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html index 0f0a626bb..52eb57759 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html @@ -1,27 +1,27 @@ - Ombi + {{applicationName}} - - find_replace - Discover - - - search Search - - - list Requests - - - settings Settings - + + + {{nav.icon}} +  {{nav.name | translate}} + - + exit_to_app {{ 'NavigationBar.Logout' | translate }} + + + {{ 'NavigationBar.ChangeTheme' | translate }} + + + + diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss new file mode 100644 index 000000000..af226e40a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -0,0 +1,58 @@ +@import "~styles/variables.scss"; +.sidenav-container { + min-height: 100vh; +} + +.sidenav { + width: 200px; +} + +.sidenav .mat-toolbar { + background: inherit; +} + +.mat-toolbar.mat-primary { + position: sticky; + top: 0; + z-index: 1; +} + +.middle { + display: flex; +} + +.example-form { + min-width: 150px; + max-width: 500px; + width: 100%; +} + +.quater-width { + width: 25%; +} + +.autocomplete-img { + vertical-align: middle; + height: 63px; +} + +.mat-option { + height: 50px; + line-height: 50px; + padding: 0px 5px; +} + +.active-list-item { + background: $accent !important; + color:white; +} + +.active-list-item-dark { + background: $accent-dark !important; + color:black; +} + +.bottom-nav-link { + bottom: 0; + position: absolute; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts index 0a9c89583..a3825d4e8 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts @@ -1,28 +1,70 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { INavBar } from '../interfaces/ICommon'; +import { StorageService } from '../shared/storage/storage-service'; @Component({ selector: 'app-my-nav', templateUrl: './my-nav.component.html', - styleUrls: ['./my-nav.component.css'], + styleUrls: ['./my-nav.component.scss'], }) -export class MyNavComponent { +export class MyNavComponent implements OnInit { isHandset$: Observable = this.breakpointObserver.observe(Breakpoints.Handset) .pipe( map(result => result.matches) ); - @Input() public showNav: boolean; - @Input() public username: string; - @Output() public logoutClick = new EventEmitter(); + @Input() public showNav: boolean; + @Input() public applicationName: string; + @Input() public username: string; + @Input() public isAdmin: string; + @Output() public logoutClick = new EventEmitter(); + @Output() public themeChange = new EventEmitter(); + public theme: string; - constructor(private breakpointObserver: BreakpointObserver) { + constructor(private breakpointObserver: BreakpointObserver, + private store: StorageService) { } + public ngOnInit(): void { + this.theme = this.store.get("theme"); + if(!this.theme) { + this.store.save("theme","light"); + } + } + + public navItems: INavBar[] = [ + { name: "NavigationBar.Discover", icon: "find_replace", link: "/discover", requiresAdmin: false }, + { name: "NavigationBar.Requests", icon: "list", link: "/requests-list", requiresAdmin: false }, + { name: "NavigationBar.UserManagement", icon: "account_circle", link: "/usermanagement", requiresAdmin: true }, + { name: "NavigationBar.Calendar", icon: "calendar_today", link: "/calendar", requiresAdmin: false }, + { name: "NavigationBar.Settings", icon: "settings", link: "/Settings/About", requiresAdmin: true }, + { name: "NavigationBar.UserPreferences", icon: "person", link: "/user-preferences", requiresAdmin: false }, + ] + public logOut() { this.logoutClick.emit(); } + + public switchTheme() { + if (this.theme) { + let newTheme = ""; + if (this.theme === "dark") { + newTheme = "light"; + } else { + newTheme = "dark"; + } + this.store.save("theme", newTheme) + this.theme = newTheme; + this.themeChange.emit(newTheme); + } + } + + public getTheme(){ + return this.theme === 'dark' ? 'active-list-item-dark' : 'active-list-item'; + } + } diff --git a/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.html b/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.html index c5b907791..0057aabbb 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.html @@ -1,3 +1,25 @@ - + + + +
+   + + {{result.title}} ({{result.release_date.slice(0,4)}}) + + {{result.title}} +
+
+   + + {{result.name}} ({{result.release_date.slice(0,4)}}) + + {{result.name}} +
+ + + + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.scss b/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.scss index c05ab8a99..e66804bdd 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.scss @@ -1,4 +1,5 @@ $ombi-primary:#3f3f3f; +$ombi-primary-darker:#2b2b2b; $ombi-accent: #258a6d; .quater-width { @@ -18,9 +19,8 @@ $ombi-accent: #258a6d; ::ng-deep ngb-typeahead-window.dropdown-menu { background-color: $ombi-primary; - width: 24%; overflow: auto; - height: 500px; + height: 33em; } ::ng-deep ngb-typeahead-window button.dropdown-item { @@ -30,4 +30,12 @@ $ombi-accent: #258a6d; ::ng-deep ngb-typeahead-window .dropdown-item.active, .dropdown-item:active { text-decoration: none; background-color: $ombi-accent; +} + +.search-bar { + background-color: $ombi-primary-darker; + border: solid 1px $ombi-primary-darker; +} +.search-bar:focus { + background-color:white; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.ts b/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.ts index 13cad8001..e3a1d0800 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/nav-search.component.ts @@ -20,8 +20,21 @@ export class NavSearchComponent { public searching = false; public searchFailed = false; - - public formatter = (result: IMultiSearchResult) => result.media_type == "movie" ? result.title + ` (${result.release_date.slice(0,4)})` : result.name + ` (${result.release_date.slice(0,4)})`; + public formatter = (result: IMultiSearchResult) => { + if(result.media_type === "movie") { + let title = result.title; + if(result.release_date) { + title += ` (${result.release_date.slice(0,4)})`; + } + return title; + } else { + let title = result.name; + if(result.release_date) { + title += ` (${result.release_date.slice(0,4)})`; + } + return title; + } + } public searchModel = (text$: Observable) => text$.pipe( @@ -42,6 +55,9 @@ export class NavSearchComponent { if (event.item.media_type == "movie") { this.router.navigate([`details/movie/${event.item.id}`]); return; + } else if (event.item.media_type == "tv") { + this.router.navigate([`details/tv/${event.item.id}/true`]); + return; } } } diff --git a/src/Ombi/ClientApp/src/app/request-grid/request-card.component.html b/src/Ombi/ClientApp/src/app/request-grid/request-card.component.html deleted file mode 100644 index f51a9f898..000000000 --- a/src/Ombi/ClientApp/src/app/request-grid/request-card.component.html +++ /dev/null @@ -1,45 +0,0 @@ - - -
-
- poster -
-
- {{request.title}} -
-
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/request-grid/request-card.component.ts b/src/Ombi/ClientApp/src/app/request-grid/request-card.component.ts deleted file mode 100644 index aa267ecdb..000000000 --- a/src/Ombi/ClientApp/src/app/request-grid/request-card.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { Component, Input } from "@angular/core"; - -// import { IMediaBase } from "../interfaces"; - -// @Component({ -// selector: "request-card", -// templateUrl: "./request-card.component.html", -// }) -// export class RequestCardComponent { -// @Input() public request: IMediaBase; -// } diff --git a/src/Ombi/ClientApp/src/app/request-grid/request-grid.component.html b/src/Ombi/ClientApp/src/app/request-grid/request-grid.component.html deleted file mode 100644 index eecd799ed..000000000 --- a/src/Ombi/ClientApp/src/app/request-grid/request-grid.component.html +++ /dev/null @@ -1,54 +0,0 @@ - - - -
-
-

New Requests

-
-
- -
-
-
- -
-

Approved Requests

-
-
- -
-
-
- -
-

Available

-
-
- -
-
-
-
diff --git a/src/Ombi/ClientApp/src/app/request-grid/request-grid.component.ts b/src/Ombi/ClientApp/src/app/request-grid/request-grid.component.ts deleted file mode 100644 index 142bbf5a1..000000000 --- a/src/Ombi/ClientApp/src/app/request-grid/request-grid.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -//import { Component, OnInit } from '@angular/core'; -//import { DragulaService } from 'ng2-dragula/ng2-dragula'; -//import { RequestService } from '../services"; -//import { ITvRequests, IMovieRequests, IRequestGrid } from '../interfaces"; - -//@Component({ -// templateUrl: './request-grid.component.html' -//}) -//export class RequestGridComponent implements OnInit { - -// constructor(private dragulaService: DragulaService, private requestService: RequestService) { -// this.dragulaService.drop.subscribe((value: any) => { -// }); -// } - -// ngOnInit() { -// this.requestService.getMovieGrid().subscribe(x => { -// this.movieRequests = x; -// }); -// this.requestService.getTvGrid().subscribe(x => { -// this.tvRequests = x; -// }); -// } - -// movieRequests: IRequestGrid; -// tvRequests: IRequestGrid; - -//} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.html new file mode 100644 index 000000000..ae5bbe1f9 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.html @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.scss b/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.scss new file mode 100644 index 000000000..497a59e76 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.scss @@ -0,0 +1,12 @@ +.loading-shade { + position: absolute; + top: 0; + left: 0; + bottom: 56px; + right: 0; + background: rgba(0, 0, 0, 0.15); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.ts new file mode 100644 index 000000000..d3b0f78fd --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/grid-spinner/grid-spinner.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from "@angular/core"; + + +@Component({ + templateUrl: "./grid-spinner.component.html", + selector: "grid-spinner", + styleUrls: ["./grid-spinner.component.scss"] +}) +export class GridSpinnerComponent{ + @Input() public loading = false; +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/index.ts b/src/Ombi/ClientApp/src/app/requests-list/components/index.ts new file mode 100644 index 000000000..7f226fea5 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/index.ts @@ -0,0 +1,20 @@ +import { RequestsListComponent } from "./requests-list.component"; +import { MoviesGridComponent } from "./movies-grid/movies-grid.component"; + +import { RequestServiceV2 } from "../../services/requestV2.service"; +import { RequestService } from "../../services"; +import { TvGridComponent } from "./tv-grid/tv-grid.component"; +import { GridSpinnerComponent } from "./grid-spinner/grid-spinner.component"; + +export const components: any[] = [ + RequestsListComponent, + MoviesGridComponent, + TvGridComponent, + GridSpinnerComponent, +]; + + +export const providers: any[] = [ + RequestService, + RequestServiceV2, +]; diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html new file mode 100644 index 000000000..3c3c975af --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html @@ -0,0 +1,64 @@ +
+ + + + + 10 + 15 + 30 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Requested By {{element.requestedUser?.userAlias}} Title {{element.title}} ({{element.releaseDate | amLocal | amDateFormat: + 'YYYY'}}) Request Date {{element.requestedDate | amLocal | amDateFormat: 'LL'}} Status {{element.status}} Request Status +
{{'Common.ProcessingRequest' | translate}}
+
{{'Common.PendingApproval' | + translate}} +
+
{{'Common.NotRequested' | + translate}} +
+
+ +
+ + +
diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts new file mode 100644 index 000000000..bec54739c --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts @@ -0,0 +1,59 @@ +import { Component, AfterViewInit, ViewChild } from "@angular/core"; +import { IMovieRequests, IRequestsViewModel } from "../../../interfaces"; +import { MatPaginator, MatSort } from "@angular/material"; +import { merge, Observable, of as observableOf } from 'rxjs'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; + +import { RequestServiceV2 } from "../../../services/requestV2.service"; + +@Component({ + templateUrl: "./movies-grid.component.html", + selector: "movies-grid", + styleUrls: ["../requests-list.component.scss"] +}) +export class MoviesGridComponent implements AfterViewInit { + public dataSource: IMovieRequests[] = []; + public resultsLength: number; + public isLoadingResults = true; + public displayedColumns: string[] = ['requestedUser.requestedBy', 'title', 'requestedDate', 'status', 'requestStatus', 'actions']; + public gridCount: string = "15"; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + constructor(private requestService: RequestServiceV2) { + + } + + public async ngAfterViewInit() { + // const results = await this.requestService.getMovieRequests(this.gridCount, 0, OrderType.RequestedDateDesc, + // { availabilityFilter: FilterType.None, statusFilter: FilterType.None }).toPromise(); + // this.dataSource = results.collection; + // this.resultsLength = results.total; + + // If the user changes the sort order, reset back to the first page. + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + startWith({}), + switchMap(() => { + this.isLoadingResults = true; + // eturn this.exampleDatabase!.getRepoIssues( + // this.sort.active, this.sort.direction, this.paginator.pageIndex); + return this.requestService.getMovieRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction); + }), + map((data: IRequestsViewModel) => { + // Flip flag to show that loading has finished. + this.isLoadingResults = false; + this.resultsLength = data.total; + + return data.collection; + }), + catchError((err) => { + this.isLoadingResults = false; + return observableOf([]); + }) + ).subscribe(data => this.dataSource = data); + } +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html new file mode 100644 index 000000000..4c0111d06 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html @@ -0,0 +1,19 @@ +
+ + + + + + + + + + + + +

Some more tab content

+

...

+
+
+ +
diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.scss b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.scss new file mode 100644 index 000000000..c875fa54c --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.scss @@ -0,0 +1,4 @@ +.small-middle-container{ + margin: auto; + width: 95%; +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts new file mode 100644 index 000000000..6489b6208 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; + +@Component({ + templateUrl: "./requests-list.component.html", + styleUrls: ["./requests-list.component.scss"] +}) +export class RequestsListComponent { +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html new file mode 100644 index 000000000..f13a0feba --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html @@ -0,0 +1,69 @@ +
+ + + + + + 10 + 15 + 30 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Series {{element.parentRequest.title}} Requested By {{element.requestedUser.userAlias}} Status + {{element.parentRequest.status}} + Requested Date + {{element.requestedDate | amLocal | amDateFormat: 'LL'}} + Request Status +
{{'Common.ProcessingRequest' | translate}}
+
{{'Common.PendingApproval' | + translate}} +
+
{{'Common.NotRequested' | + translate}} +
+
+ +
+ + +
diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts new file mode 100644 index 000000000..536265622 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts @@ -0,0 +1,53 @@ +import { Component, AfterViewInit, ViewChild } from "@angular/core"; +import { IRequestsViewModel, ITvRequests, IChildRequests } from "../../../interfaces"; +import { MatPaginator, MatSort } from "@angular/material"; +import { merge, Observable, of as observableOf } from 'rxjs'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; + +import { RequestServiceV2 } from "../../../services/requestV2.service"; + +@Component({ + templateUrl: "./tv-grid.component.html", + selector: "tv-grid", + styleUrls: ["../requests-list.component.scss"] +}) +export class TvGridComponent implements AfterViewInit { + public dataSource: IChildRequests[] = []; + public resultsLength: number; + public isLoadingResults = true; + public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions']; + public gridCount: string = "15"; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + constructor(private requestService: RequestServiceV2) { + + } + + public async ngAfterViewInit() { + + // If the user changes the sort order, reset back to the first page. + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + startWith({}), + switchMap(() => { + this.isLoadingResults = true; + return this.requestService.getTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction); + }), + map((data: IRequestsViewModel) => { + // Flip flag to show that loading has finished. + this.isLoadingResults = false; + this.resultsLength = data.total; + + return data.collection; + }), + catchError((err) => { + this.isLoadingResults = false; + return observableOf([]); + }) + ).subscribe(data => this.dataSource = data); + } +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/requests-list.module.ts b/src/Ombi/ClientApp/src/app/requests-list/requests-list.module.ts new file mode 100644 index 000000000..c6d7157c8 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/requests-list.module.ts @@ -0,0 +1,35 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + + +import { SharedModule } from "../shared/shared.module"; +import { PipeModule } from "../pipes/pipe.module"; + +import { AuthGuard } from "../auth/auth.guard"; + +import * as fromComponents from './components'; +import { RequestsListComponent } from "./components/requests-list.component"; + +const routes: Routes = [ + { path: "", component: RequestsListComponent, canActivate: [AuthGuard] }, +]; +@NgModule({ + imports: [ + RouterModule.forChild(routes), + SharedModule, + PipeModule, + ], + declarations: [ + ...fromComponents.components + ], + exports: [ + RouterModule, + ], + entryComponents: [ + ], + providers: [ + ...fromComponents.providers + ], + +}) +export class RequestsListModule { } diff --git a/src/Ombi/ClientApp/src/app/requests/movierequests.component.ts b/src/Ombi/ClientApp/src/app/requests/movierequests.component.ts index 3459ac0c7..69a2359cf 100644 --- a/src/Ombi/ClientApp/src/app/requests/movierequests.component.ts +++ b/src/Ombi/ClientApp/src/app/requests/movierequests.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, Input, OnInit, Inject } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { Subject } from "rxjs"; import { debounceTime, distinctUntilChanged } from "rxjs/operators"; @@ -44,6 +44,7 @@ export class MovieRequestsComponent implements OnInit { public totalMovies: number = 100; public currentlyLoaded: number; private amountToLoad: number; + private href: string; constructor( private requestService: RequestService, @@ -51,7 +52,8 @@ export class MovieRequestsComponent implements OnInit { private notificationService: NotificationService, private radarrService: RadarrService, private sanitizer: DomSanitizer, - private readonly platformLocation: PlatformLocation) { + @Inject(APP_BASE_HREF) href:string) { + this.href = href; this.searchChanged.pipe( debounceTime(600), // Wait Xms after the last event before emitting last event distinctUntilChanged(), // only emit if value is different from previous value @@ -68,7 +70,7 @@ export class MovieRequestsComponent implements OnInit { }); }); this.defaultPoster = "../../../images/default_movie_poster.png"; - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = this.href; if (base) { this.defaultPoster = "../../.." + base + "/images/default_movie_poster.png"; } diff --git a/src/Ombi/ClientApp/src/app/requests/music/musicrequests.component.ts b/src/Ombi/ClientApp/src/app/requests/music/musicrequests.component.ts index b10173042..8d8336dcb 100644 --- a/src/Ombi/ClientApp/src/app/requests/music/musicrequests.component.ts +++ b/src/Ombi/ClientApp/src/app/requests/music/musicrequests.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, Input, OnInit, Inject } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { Subject } from "rxjs"; import { debounceTime, distinctUntilChanged } from "rxjs/operators"; @@ -41,13 +41,15 @@ export class MusicRequestsComponent implements OnInit { public totalAlbums: number = 100; public currentlyLoaded: number; private amountToLoad: number; + private href: string; constructor( private requestService: RequestService, private auth: AuthService, private notificationService: NotificationService, private sanitizer: DomSanitizer, - private readonly platformLocation: PlatformLocation) { + @Inject(APP_BASE_HREF) href:string) { + this.href = href; this.searchChanged.pipe( debounceTime(600), // Wait Xms after the last event before emitting last event distinctUntilChanged(), // only emit if value is different from previous value @@ -64,7 +66,7 @@ export class MusicRequestsComponent implements OnInit { }); }); this.defaultPoster = "../../../images/default-music-placeholder.png"; - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = this.href; if (base) { this.defaultPoster = "../../.." + base + "/images/default-music-placeholder.png"; } diff --git a/src/Ombi/ClientApp/src/app/requests/tvrequests.component.ts b/src/Ombi/ClientApp/src/app/requests/tvrequests.component.ts index 924fccd1f..4ecf1e6ac 100644 --- a/src/Ombi/ClientApp/src/app/requests/tvrequests.component.ts +++ b/src/Ombi/ClientApp/src/app/requests/tvrequests.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, Input, OnInit, Inject } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { Subject } from "rxjs"; import { debounceTime, distinctUntilChanged } from "rxjs/operators"; @@ -38,6 +38,7 @@ export class TvRequestsComponent implements OnInit { public totalTv: number = 100; private currentlyLoaded: number; private amountToLoad: number; + private href: string; constructor( private requestService: RequestService, @@ -46,8 +47,8 @@ export class TvRequestsComponent implements OnInit { private imageService: ImageService, private sonarrService: SonarrService, private notificationService: NotificationService, - private readonly platformLocation: PlatformLocation) { - + @Inject(APP_BASE_HREF) href:string) { + this.href= href; this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.currentUser = this.auth.claims().name; if (this.isAdmin) { @@ -86,7 +87,7 @@ export class TvRequestsComponent implements OnInit { }); }); this.defaultPoster = "../../../images/default_tv_poster.png"; - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = this.href; if (base) { this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png"; } diff --git a/src/Ombi/ClientApp/src/app/search/moviesearch.component.html b/src/Ombi/ClientApp/src/app/search/moviesearch.component.html index c19815678..f28650932 100644 --- a/src/Ombi/ClientApp/src/app/search/moviesearch.component.html +++ b/src/Ombi/ClientApp/src/app/search/moviesearch.component.html @@ -101,10 +101,10 @@ class="label label-info" [translate]="'Search.Movies.Trailer'"> {{result.quality}}p - - + + + + languageData; this.searchChanged.pipe( debounceTime(600), // Wait Xms after the last event before emitting last event @@ -54,7 +56,7 @@ export class MovieSearchComponent implements OnInit { this.runSearch(); }); this.defaultPoster = "../../../images/default_movie_poster.png"; - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = this.href; if (base) { this.defaultPoster = "../../.." + base + "/images/default_movie_poster.png"; } diff --git a/src/Ombi/ClientApp/src/app/search/music/albumsearch.component.html b/src/Ombi/ClientApp/src/app/search/music/albumsearch.component.html index 8f2fe73ce..2b31df040 100644 --- a/src/Ombi/ClientApp/src/app/search/music/albumsearch.component.html +++ b/src/Ombi/ClientApp/src/app/search/music/albumsearch.component.html @@ -80,7 +80,7 @@ {{ 'Common.Available' | translate }}
-
+
diff --git a/src/Ombi/ClientApp/src/app/search/music/musicsearch.component.ts b/src/Ombi/ClientApp/src/app/search/music/musicsearch.component.ts index 2fd9839f2..a5157d6a6 100644 --- a/src/Ombi/ClientApp/src/app/search/music/musicsearch.component.ts +++ b/src/Ombi/ClientApp/src/app/search/music/musicsearch.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, Input, OnInit, Inject } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { Subject } from "rxjs"; import { debounceTime, distinctUntilChanged } from "rxjs/operators"; @@ -31,10 +31,11 @@ export class MusicSearchComponent implements OnInit { public issueCategorySelected: IIssueCategory; public defaultPoster: string; + private href: string; constructor( private searchService: SearchService, private sanitizer: DomSanitizer, - private platformLocation: PlatformLocation) { - + @Inject(APP_BASE_HREF) href:string) { +this.href = href; this.searchChanged.pipe( debounceTime(600), // Wait Xms after the last event before emitting last event distinctUntilChanged(), // only emit if value is different from previous value @@ -69,7 +70,7 @@ export class MusicSearchComponent implements OnInit { } }); this.defaultPoster = "../../../images/default-music-placeholder.png"; - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = this.href; if (base) { this.defaultPoster = "../../.." + base + "/images/default-music-placeholder.png"; } diff --git a/src/Ombi/ClientApp/src/app/search/seriesinformation.component.html b/src/Ombi/ClientApp/src/app/search/seriesinformation.component.html index 2ae6408c7..12059e417 100644 --- a/src/Ombi/ClientApp/src/app/search/seriesinformation.component.html +++ b/src/Ombi/ClientApp/src/app/search/seriesinformation.component.html @@ -42,8 +42,11 @@ {{ep.title}} - + {{ep.airDate | amLocal | amDateFormat: 'L' }} + + + {{ep.airDateDisplay }} Available diff --git a/src/Ombi/ClientApp/src/app/search/tvsearch.component.ts b/src/Ombi/ClientApp/src/app/search/tvsearch.component.ts index 92d0d549a..5f65f3192 100644 --- a/src/Ombi/ClientApp/src/app/search/tvsearch.component.ts +++ b/src/Ombi/ClientApp/src/app/search/tvsearch.component.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Component, Input, OnInit, Inject } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { Subject } from "rxjs"; import { debounceTime, distinctUntilChanged } from "rxjs/operators"; @@ -30,13 +30,14 @@ export class TvSearchComponent implements OnInit { public issueRequestId: number; public issueProviderId: string; public issueCategorySelected: IIssueCategory; + private href: string; constructor( private searchService: SearchService, private requestService: RequestService, private notificationService: NotificationService, private authService: AuthService, private imageService: ImageService, private sanitizer: DomSanitizer, - private readonly platformLocation: PlatformLocation) { - + @Inject(APP_BASE_HREF) href:string) { +this.href = href; this.searchChanged.pipe( debounceTime(600), // Wait Xms after the last event before emitting last event distinctUntilChanged(), // only emit if value is different from previous value @@ -54,7 +55,7 @@ export class TvSearchComponent implements OnInit { }); }); this.defaultPoster = "../../../images/default_tv_poster.png"; - const base = this.platformLocation.getBaseHrefFromDOM(); + const base = this.href; if (base) { this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png"; } diff --git a/src/Ombi/ClientApp/src/app/services/applications/couchpotato.service.ts b/src/Ombi/ClientApp/src/app/services/applications/couchpotato.service.ts index 44fa409a4..881aaa8a9 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/couchpotato.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/couchpotato.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { ServiceHelpers } from "../service.helpers"; @@ -9,8 +9,8 @@ import { ICouchPotatoApiKey, ICouchPotatoProfiles, ICouchPotatoSettings } from " @Injectable() export class CouchPotatoService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/CouchPotato/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/CouchPotato/", href); } public getProfiles(settings: ICouchPotatoSettings): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts b/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts index d4e52a630..6c519607c 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { ServiceHelpers } from "../service.helpers"; @@ -9,8 +9,8 @@ import { IEmbySettings, IUsersModel } from "../../interfaces"; @Injectable() export class EmbyService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Emby/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Emby/", href); } public logIn(settings: IEmbySettings): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts b/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts index 9ef36357a..b4e4830d3 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { ILidarrProfile, ILidarrRootFolder, IProfiles } from "../../interfaces"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "../service.helpers"; @Injectable() export class LidarrService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Lidarr", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Lidarr", href); } public getRootFolders(settings: ILidarrSettings): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/plex.service.ts b/src/Ombi/ClientApp/src/app/services/applications/plex.service.ts index 9f53d0e34..39bfea53e 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/plex.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/plex.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; @@ -10,8 +10,8 @@ import { IPlexAuthentication, IPlexLibResponse, IPlexLibSimpleResponse, IPlexOAu @Injectable() export class PlexService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Plex/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Plex/", href); } public logIn(login: string, password: string): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/plexoauth.service.ts b/src/Ombi/ClientApp/src/app/services/applications/plexoauth.service.ts index 7a1b495b5..770aad6f9 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/plexoauth.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/plexoauth.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; @@ -10,8 +10,8 @@ import { IPlexOAuthAccessToken } from "../../interfaces"; @Injectable() export class PlexOAuthService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/PlexOAuth/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/PlexOAuth/", href); } public oAuth(pin: number): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts b/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts index 03a49e90c..f214ac291 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; @@ -9,7 +9,7 @@ import { IPlexPin } from "../../interfaces"; @Injectable() export class PlexTvService { - constructor(private http: HttpClient, public platformLocation: PlatformLocation) { + constructor(private http: HttpClient) { } public GetPin(clientId: string, applicationName: string): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/radarr.service.ts b/src/Ombi/ClientApp/src/app/services/applications/radarr.service.ts index 140a59d28..eaa785233 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/radarr.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/radarr.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { IRadarrProfile, IRadarrRootFolder } from "../../interfaces"; @@ -9,21 +9,24 @@ import { ServiceHelpers } from "../service.helpers"; @Injectable() export class RadarrService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Radarr", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Radarr", href); } public getRootFolders(settings: IRadarrSettings): Observable { - return this.http.post(`${this.url}/RootFolders/`, JSON.stringify(settings), {headers: this.headers}); + return this.http.post(`${this.url}/RootFolders/`, JSON.stringify(settings), { headers: this.headers }); } public getQualityProfiles(settings: IRadarrSettings): Observable { - return this.http.post(`${this.url}/Profiles/`, JSON.stringify(settings), {headers: this.headers}); + return this.http.post(`${this.url}/Profiles/`, JSON.stringify(settings), { headers: this.headers }); } public getRootFoldersFromSettings(): Observable { - return this.http.get(`${this.url}/RootFolders/`, {headers: this.headers}); + return this.http.get(`${this.url}/RootFolders/`, { headers: this.headers }); } public getQualityProfilesFromSettings(): Observable { - return this.http.get(`${this.url}/Profiles/`, {headers: this.headers}); + return this.http.get(`${this.url}/Profiles/`, { headers: this.headers }); + } + public isRadarrEnabled(): Promise { + return this.http.get(`${this.url}/enabled/`, { headers: this.headers }).toPromise(); } } diff --git a/src/Ombi/ClientApp/src/app/services/applications/sonarr.service.ts b/src/Ombi/ClientApp/src/app/services/applications/sonarr.service.ts index bce47acf9..4745dbebd 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/sonarr.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/sonarr.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -10,8 +10,8 @@ import { ServiceHelpers } from "../service.helpers"; @Injectable() export class SonarrService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Sonarr", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Sonarr", href); } public getRootFolders(settings: ISonarrSettings): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts b/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts index e692b9196..1b7c55821 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -11,6 +11,7 @@ import { IDiscordNotifcationSettings, IEmailNotificationSettings, IEmbyServer, + IGotifyNotificationSettings, ILidarrSettings, IMattermostNotifcationSettings, IMobileNotificationTestSettings, @@ -27,8 +28,8 @@ import { @Injectable() export class TesterService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/tester/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/tester/", href); } public discordTest(settings: IDiscordNotifcationSettings): Observable { @@ -40,7 +41,11 @@ export class TesterService extends ServiceHelpers { } public pushoverTest(settings: IPushoverNotificationSettings): Observable { - return this.http.post(`${this.url}pushover`, JSON.stringify(settings), {headers: this.headers}); + return this.http.post(`${this.url}pushover`, JSON.stringify(settings), { headers: this.headers }); + } + + public gotifyTest(settings: IGotifyNotificationSettings): Observable { + return this.http.post(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers }); } public mattermostTest(settings: IMattermostNotifcationSettings): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/calendar.service.ts b/src/Ombi/ClientApp/src/app/services/calendar.service.ts new file mode 100644 index 000000000..ba54d0048 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/services/calendar.service.ts @@ -0,0 +1,18 @@ +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; + +import { HttpClient } from "@angular/common/http"; +import { Observable } from "rxjs"; + +import { ServiceHelpers } from "./service.helpers"; +import { ICalendarModel } from "../interfaces/ICalendar"; + +@Injectable() +export class CalendarService extends ServiceHelpers { + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v2/Calendar/", href); + } + public getCalendarEntries(): Promise { + return this.http.get(`${this.url}`, {headers: this.headers}).toPromise(); + } +} diff --git a/src/Ombi/ClientApp/src/app/services/custompage.service.ts b/src/Ombi/ClientApp/src/app/services/custompage.service.ts new file mode 100644 index 000000000..d89fd6122 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/services/custompage.service.ts @@ -0,0 +1,25 @@ +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { HttpClient } from "@angular/common/http"; +import { Injectable, Inject } from "@angular/core"; +import { Observable } from "rxjs"; + +import { + ICustomPage, +} from "../interfaces"; + +import { ServiceHelpers } from "./service.helpers"; + +@Injectable() +export class CustomPageService extends ServiceHelpers { + constructor(public http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/CustomPage", href); + } + + public getCustomPage(): Observable { + return this.http.get(this.url, {headers: this.headers}); + } + + public saveCustomPage(model: ICustomPage): Observable { + return this.http.post(this.url, model, {headers: this.headers}); + } +} diff --git a/src/Ombi/ClientApp/src/app/services/identity.service.ts b/src/Ombi/ClientApp/src/app/services/identity.service.ts index 670c7f9bb..37e8afbda 100644 --- a/src/Ombi/ClientApp/src/app/services/identity.service.ts +++ b/src/Ombi/ClientApp/src/app/services/identity.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class IdentityService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Identity/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Identity/", href); } public createWizardUser(user: ICreateWizardUser): Observable { return this.http.post(`${this.url}Wizard/`, JSON.stringify(user), {headers: this.headers}); @@ -78,15 +78,4 @@ export class IdentityService extends ServiceHelpers { public getNotificationPreferencesForUser(userId: string): Observable { return this.http.get(`${this.url}notificationpreferences/${userId}`, {headers: this.headers}); } - - public hasRole(role: string): boolean { - const roles = localStorage.getItem("roles") as any as string[] | null; - if (roles) { - if (roles.indexOf(role) > -1) { - return true; - } - return false; - } - return false; - } } diff --git a/src/Ombi/ClientApp/src/app/services/image.service.ts b/src/Ombi/ClientApp/src/app/services/image.service.ts index 92119f0a0..521a207e3 100644 --- a/src/Ombi/ClientApp/src/app/services/image.service.ts +++ b/src/Ombi/ClientApp/src/app/services/image.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { HttpClient } from "@angular/common/http"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class ImageService extends ServiceHelpers { - constructor(public http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Images/", platformLocation); + constructor(public http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Images/", href); } public getRandomBackground(): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/index.ts b/src/Ombi/ClientApp/src/app/services/index.ts index 8a97dd900..911f41910 100644 --- a/src/Ombi/ClientApp/src/app/services/index.ts +++ b/src/Ombi/ClientApp/src/app/services/index.ts @@ -17,3 +17,5 @@ export * from "./recentlyAdded.service"; export * from "./vote.service"; export * from "./requestretry.service"; export * from "./searchV2.service"; +export * from "./custompage.service"; +export * from "./message.service"; diff --git a/src/Ombi/ClientApp/src/app/services/issues.service.ts b/src/Ombi/ClientApp/src/app/services/issues.service.ts index 41cbb4df1..4500fb90c 100644 --- a/src/Ombi/ClientApp/src/app/services/issues.service.ts +++ b/src/Ombi/ClientApp/src/app/services/issues.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class IssuesService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Issues/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Issues/", href); } public getCategories(): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/job.service.ts b/src/Ombi/ClientApp/src/app/services/job.service.ts index 05425683f..c855b354b 100644 --- a/src/Ombi/ClientApp/src/app/services/job.service.ts +++ b/src/Ombi/ClientApp/src/app/services/job.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -8,8 +8,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class JobService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Job/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Job/", href); } public forceUpdate(): Observable { return this.http.post(`${this.url}update/`, {headers: this.headers}); diff --git a/src/Ombi/ClientApp/src/app/services/landingpage.service.ts b/src/Ombi/ClientApp/src/app/services/landingpage.service.ts index 22964ff71..f2e7f7967 100644 --- a/src/Ombi/ClientApp/src/app/services/landingpage.service.ts +++ b/src/Ombi/ClientApp/src/app/services/landingpage.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { HttpClient } from "@angular/common/http"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class LandingPageService extends ServiceHelpers { - constructor(public http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/LandingPage/", platformLocation); + constructor(public http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/LandingPage/", href); } public getServerStatus(): Observable { diff --git a/src/Ombi/ClientApp/src/app/services/message.service.ts b/src/Ombi/ClientApp/src/app/services/message.service.ts new file mode 100644 index 000000000..0886e6b78 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/services/message.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from "@angular/core"; +import { MatSnackBar, MatSnackBarConfig } from "@angular/material"; + +@Injectable() +export class MessageService { + constructor(private snackBar: MatSnackBar) { + this.config = { + duration: 4000, + } + } + private config: MatSnackBarConfig; + + + public send(message: string, action?: string) { + if (action) { + this.snackBar.open(message, action.toUpperCase(), this.config) + } else { + this.snackBar.open(message, "OK", this.config) + } + } +} diff --git a/src/Ombi/ClientApp/src/app/services/mobile.service.ts b/src/Ombi/ClientApp/src/app/services/mobile.service.ts index 1f9e3fb24..177f1f35f 100644 --- a/src/Ombi/ClientApp/src/app/services/mobile.service.ts +++ b/src/Ombi/ClientApp/src/app/services/mobile.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -9,10 +9,14 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class MobileService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/mobile/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/mobile/", href); } public getUserDeviceList(): Observable { return this.http.get(`${this.url}notification/`, {headers: this.headers}); } + + public deleteUser(userId: string): Observable { + return this.http.post(`${this.url}remove/`, userId, {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/src/app/services/notificationMessage.service.ts b/src/Ombi/ClientApp/src/app/services/notificationMessage.service.ts index 93727c5d2..046f14bf0 100644 --- a/src/Ombi/ClientApp/src/app/services/notificationMessage.service.ts +++ b/src/Ombi/ClientApp/src/app/services/notificationMessage.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -10,8 +10,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class NotificationMessageService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/notifications/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/notifications/", href); } public sendMassEmail(model: IMassEmailModel): Observable { return this.http.post(`${this.url}massemail/`, JSON.stringify(model) ,{headers: this.headers}); diff --git a/src/Ombi/ClientApp/src/app/services/recentlyAdded.service.ts b/src/Ombi/ClientApp/src/app/services/recentlyAdded.service.ts index c062f973b..3183f9cee 100644 --- a/src/Ombi/ClientApp/src/app/services/recentlyAdded.service.ts +++ b/src/Ombi/ClientApp/src/app/services/recentlyAdded.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class RecentlyAddedService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/recentlyadded/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/recentlyadded/", href); } public getRecentlyAddedMovies(): Observable { return this.http.get(`${this.url}movies/`, {headers: this.headers}); diff --git a/src/Ombi/ClientApp/src/app/services/request.service.ts b/src/Ombi/ClientApp/src/app/services/request.service.ts index f7e2b3875..7f15dfdba 100644 --- a/src/Ombi/ClientApp/src/app/services/request.service.ts +++ b/src/Ombi/ClientApp/src/app/services/request.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -14,8 +14,8 @@ import { IRemainingRequests } from "../interfaces/IRemainingRequests"; @Injectable() export class RequestService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Request/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Request/", href); } public getRemainingMovieRequests(): Observable { @@ -70,6 +70,10 @@ export class RequestService extends ServiceHelpers { return this.http.get(`${this.url}movie/search/${search}`, {headers: this.headers}); } + public getMovieRequest(requestId: number): Promise { + return this.http.get(`${this.url}movie/info/${requestId}`, {headers: this.headers}).toPromise(); + } + public removeMovieRequest(request: IMovieRequests) { this.http.delete(`${this.url}movie/${request.id}`, {headers: this.headers}).subscribe(); } diff --git a/src/Ombi/ClientApp/src/app/services/requestV2.service.ts b/src/Ombi/ClientApp/src/app/services/requestV2.service.ts new file mode 100644 index 000000000..e3ae468b2 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/services/requestV2.service.ts @@ -0,0 +1,24 @@ +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; + +import { HttpClient } from "@angular/common/http"; +import { Observable } from "rxjs"; +import { ServiceHelpers } from "./service.helpers"; +import { IRequestsViewModel, IMovieRequests, ITvRequests, IChildRequests } from "../interfaces"; + + +@Injectable() +export class RequestServiceV2 extends ServiceHelpers { + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v2/Requests/", href); + } + + public getMovieRequests(count: number, position: number, sortProperty: string , order: string): Observable> { + return this.http.get>(`${this.url}movie/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers}); + } + + public getTvRequests(count: number, position: number, sortProperty: string , order: string): Observable> { + return this.http.get>(`${this.url}tv/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers}); + } + +} diff --git a/src/Ombi/ClientApp/src/app/services/requestretry.service.ts b/src/Ombi/ClientApp/src/app/services/requestretry.service.ts index e5c9cabe0..6c8d7040b 100644 --- a/src/Ombi/ClientApp/src/app/services/requestretry.service.ts +++ b/src/Ombi/ClientApp/src/app/services/requestretry.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -9,8 +9,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class RequestRetryService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/requestretry/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/requestretry/", href); } public getFailedRequests(): Observable { return this.http.get(this.url, {headers: this.headers}); diff --git a/src/Ombi/ClientApp/src/app/services/search.service.ts b/src/Ombi/ClientApp/src/app/services/search.service.ts index 5379e23e7..3b4da673a 100644 --- a/src/Ombi/ClientApp/src/app/services/search.service.ts +++ b/src/Ombi/ClientApp/src/app/services/search.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -12,8 +12,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class SearchService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/search", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/search", href); } // Movies diff --git a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts index 269bca75f..61ae3a471 100644 --- a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -8,11 +8,12 @@ import { IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../inte import { ServiceHelpers } from "./service.helpers"; import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2"; +import { ISearchTvResultV2, IMovieCollectionsViewModel } from "../interfaces/ISearchTvResultV2"; @Injectable() export class SearchV2Service extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v2/search", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v2/search", href); } public multiSearch(searchTerm: string): Observable { @@ -21,6 +22,9 @@ export class SearchV2Service extends ServiceHelpers { public getFullMovieDetails(theMovieDbId: number): Observable { return this.http.get(`${this.url}/Movie/${theMovieDbId}`); } + public getFullMovieDetailsPromise(theMovieDbId: number): Promise { + return this.http.get(`${this.url}/Movie/${theMovieDbId}`).toPromise(); + } public similarMovies(theMovieDbId: number, langCode: string): Observable { return this.http.post(`${this.url}/Movie/similar`, {theMovieDbId, languageCode: langCode}); @@ -30,13 +34,24 @@ export class SearchV2Service extends ServiceHelpers { return this.http.get(`${this.url}/Movie/Popular`); } + public popularMoviesByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Movie/Popular/${currentlyLoaded}/${toLoad}`).toPromise(); + } + public upcomingMovies(): Observable { return this.http.get(`${this.url}/Movie/upcoming`); } + public upcomingMoviesByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Movie/upcoming/${currentlyLoaded}/${toLoad}`).toPromise(); + } + public nowPlayingMovies(): Observable { return this.http.get(`${this.url}/Movie/nowplaying`); } + public nowPlayingMoviesByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Movie/nowplaying/${currentlyLoaded}/${toLoad}`).toPromise(); + } public topRatedMovies(): Observable { return this.http.get(`${this.url}/Movie/toprated`); @@ -45,13 +60,38 @@ export class SearchV2Service extends ServiceHelpers { public popularTv(): Observable { return this.http.get(`${this.url}/Tv/popular`, { headers: this.headers }); } + + public popularTvByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Tv/popular/${currentlyLoaded}/${toLoad}`, { headers: this.headers }).toPromise(); + } + public mostWatchedTv(): Observable { return this.http.get(`${this.url}/Tv/mostwatched`, { headers: this.headers }); } public anticipatedTv(): Observable { return this.http.get(`${this.url}/Tv/anticipated`, { headers: this.headers }); } + public anticipatedTvByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Tv/anticipated/${currentlyLoaded}/${toLoad}`, { headers: this.headers }).toPromise(); + } + public trendingTv(): Observable { return this.http.get(`${this.url}/Tv/trending`, { headers: this.headers }); } + + public trendingTvByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Tv/trending/${currentlyLoaded}/${toLoad}`, { headers: this.headers }).toPromise(); + } + + public getTvInfo(tvdbid: number): Promise { + return this.http.get(`${this.url}/Tv/${tvdbid}`, { headers: this.headers }).toPromise(); + } + + public getTvInfoWithMovieDbId(theMovieDbId: number): Promise { + return this.http.get(`${this.url}/Tv/moviedb/${theMovieDbId}`, { headers: this.headers }).toPromise(); + } + + public getMovieCollections(collectionId: number): Promise { + return this.http.get(`${this.url}/movie/collection/${collectionId}`, { headers: this.headers }).toPromise(); + } } diff --git a/src/Ombi/ClientApp/src/app/services/service.helpers.ts b/src/Ombi/ClientApp/src/app/services/service.helpers.ts index c57eae464..05fe321d2 100644 --- a/src/Ombi/ClientApp/src/app/services/service.helpers.ts +++ b/src/Ombi/ClientApp/src/app/services/service.helpers.ts @@ -4,8 +4,8 @@ import { HttpClient, HttpHeaders } from "@angular/common/http"; export class ServiceHelpers { protected headers = new HttpHeaders(); - constructor(protected http: HttpClient, protected url: string, protected platformLocation: PlatformLocation) { - const base = platformLocation.getBaseHrefFromDOM(); + constructor(protected http: HttpClient, protected url: string, protected href: string) { + const base = href; this.headers = new HttpHeaders().set("Content-Type","application/json"); if (base.length > 1) { this.url = base + this.url; diff --git a/src/Ombi/ClientApp/src/app/services/settings.service.ts b/src/Ombi/ClientApp/src/app/services/settings.service.ts index 4e68d28df..184977838 100644 --- a/src/Ombi/ClientApp/src/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/src/app/services/settings.service.ts @@ -1,6 +1,6 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Observable } from "rxjs"; import { @@ -10,11 +10,11 @@ import { ICronTestModel, ICronViewModelBody, ICustomizationSettings, - ICustomPage, IDiscordNotifcationSettings, IDogNzbSettings, IEmailNotificationSettings, IEmbySettings, + IGotifyNotificationSettings, IIssueSettings, IJobSettings, IJobSettingsViewModel, @@ -41,8 +41,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class SettingsService extends ServiceHelpers { - constructor(public http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Settings", platformLocation); + constructor(public http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Settings", href); } public about(): Observable { @@ -113,14 +113,6 @@ export class SettingsService extends ServiceHelpers { return this.http.get(`${this.url}/Authentication`, {headers: this.headers}); } - public getCustomPage(): Observable { - return this.http.get(`${this.url}/CustomPage`, {headers: this.headers}); - } - - public saveCustomPage(model: ICustomPage): Observable { - return this.http.post(`${this.url}/CustomPage`, model, {headers: this.headers}); - } - public getClientId(): Observable { return this.http.get(`${this.url}/clientid`, {headers: this.headers}); } @@ -191,6 +183,14 @@ export class SettingsService extends ServiceHelpers { .post(`${this.url}/notifications/pushover`, JSON.stringify(settings), {headers: this.headers}); } + public getGotifyNotificationSettings(): Observable { + return this.http.get(`${this.url}/notifications/gotify`, { headers: this.headers }); + } + public saveGotifyNotificationSettings(settings: IGotifyNotificationSettings): Observable { + return this.http + .post(`${this.url}/notifications/gotify`, JSON.stringify(settings), { headers: this.headers }); + } + public getSlackNotificationSettings(): Observable { return this.http.get(`${this.url}/notifications/slack`, {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/src/app/services/status.service.ts b/src/Ombi/ClientApp/src/app/services/status.service.ts index 795fb1bf1..dd252e607 100644 --- a/src/Ombi/ClientApp/src/app/services/status.service.ts +++ b/src/Ombi/ClientApp/src/app/services/status.service.ts @@ -1,5 +1,5 @@ -import { PlatformLocation } from "@angular/common"; -import { Injectable } from "@angular/core"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; @@ -8,8 +8,8 @@ import { ServiceHelpers } from "./service.helpers"; @Injectable() export class StatusService extends ServiceHelpers { - constructor(http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/status/", platformLocation); + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/status/", href); } public getWizardStatus(): Observable { return this.http.get(`${this.url}Wizard/`, {headers: this.headers}); diff --git a/src/Ombi/ClientApp/src/app/services/vote.service.ts b/src/Ombi/ClientApp/src/app/services/vote.service.ts index 375f0fc33..a670c65b8 100644 --- a/src/Ombi/ClientApp/src/app/services/vote.service.ts +++ b/src/Ombi/ClientApp/src/app/services/vote.service.ts @@ -1,14 +1,14 @@ -import { PlatformLocation } from "@angular/common"; +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { IVoteEngineResult, IVoteViewModel } from "../interfaces"; import { ServiceHelpers } from "./service.helpers"; @Injectable() export class VoteService extends ServiceHelpers { - constructor(public http: HttpClient, public platformLocation: PlatformLocation) { - super(http, "/api/v1/Vote/", platformLocation); + constructor(public http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v1/Vote/", href); } public async getModel(): Promise { diff --git a/src/Ombi/ClientApp/src/app/settings/about/about.component.html b/src/Ombi/ClientApp/src/app/settings/about/about.component.html index e05677c0f..195e59bcc 100644 --- a/src/Ombi/ClientApp/src/app/settings/about/about.component.html +++ b/src/Ombi/ClientApp/src/app/settings/about/about.component.html @@ -1,6 +1,6 @@  -
+
About
@@ -38,7 +38,7 @@ Discord - https://discord.gg/ + https://discord.gg/Sa7wNWb diff --git a/src/Ombi/ClientApp/src/app/settings/customization/customization.component.html b/src/Ombi/ClientApp/src/app/settings/customization/customization.component.html index 21a992c32..1f7a158ae 100644 --- a/src/Ombi/ClientApp/src/app/settings/customization/customization.component.html +++ b/src/Ombi/ClientApp/src/app/settings/customization/customization.component.html @@ -75,7 +75,7 @@
-
diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html index 62188aaf6..66cac8c8b 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html @@ -67,7 +67,7 @@
diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/gotify.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/gotify.component.html new file mode 100644 index 000000000..9148cb880 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/notifications/gotify.component.html @@ -0,0 +1,67 @@ + + +
+
+ Gotify Notifications +
+ + +
+
+ + +
+
+ +
+ + + + The Base URL is required +
+ +
+ + + + The Application Token is required +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+ + + +
+
+ +
+
+ +
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/gotify.component.ts b/src/Ombi/ClientApp/src/app/settings/notifications/gotify.component.ts new file mode 100644 index 000000000..f6c08d41b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/notifications/gotify.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; + +import { IGotifyNotificationSettings, INotificationTemplates, NotificationType } from "../../interfaces"; +import { TesterService } from "../../services"; +import { NotificationService } from "../../services"; +import { SettingsService } from "../../services"; + +@Component({ + templateUrl: "./gotify.component.html", +}) +export class GotifyComponent implements OnInit { + public NotificationType = NotificationType; + public templates: INotificationTemplates[]; + public form: FormGroup; + + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private fb: FormBuilder, + private testerService: TesterService) { } + + public ngOnInit() { + this.settingsService.getGotifyNotificationSettings().subscribe(x => { + this.templates = x.notificationTemplates; + + this.form = this.fb.group({ + enabled: [x.enabled], + baseUrl: [x.baseUrl, [Validators.required]], + applicationToken: [x.applicationToken, [Validators.required]], + priority: [x.priority], + }); + }); + } + + public onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + const settings = form.value; + settings.notificationTemplates = this.templates; + + this.settingsService.saveGotifyNotificationSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved the Gotify settings"); + } else { + this.notificationService.success("There was an error when saving the Gotify settings"); + } + }); + + } + + public test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + this.testerService.gotifyTest(form.value).subscribe(x => { + if (x) { + this.notificationService.success("Successfully sent a Gotify message"); + } else { + this.notificationService.error("There was an error when sending the Gotify message. Please check your settings"); + } + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/mobile.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/mobile.component.html index 5c82e03d5..2b7ea9b2c 100644 --- a/src/Ombi/ClientApp/src/app/settings/notifications/mobile.component.html +++ b/src/Ombi/ClientApp/src/app/settings/notifications/mobile.component.html @@ -35,7 +35,7 @@
- +
-
-
- -
- -
- - -
-
-
- -
-
-
-
-
-
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- -
- -
- -
-
- -
-
- -
-
+
+
+
+ + Do not send Notifications if a User has the Auto Approve permission +
+ + Hide requests from other users + +
+ + Ignore any certificate errors + +
+ + Allow us to collect anonymous analytical data e.g. browser used + +
+ + + -- + + {{lang.nativeName}} + + +
- -
\ No newline at end of file +
+
+ +
+
+
+ + diff --git a/src/Ombi/ClientApp/src/app/settings/settings.module.ts b/src/Ombi/ClientApp/src/app/settings/settings.module.ts index fb1a10abe..514bfd91b 100644 --- a/src/Ombi/ClientApp/src/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/src/app/settings/settings.module.ts @@ -27,6 +27,7 @@ import { LidarrComponent } from "./lidarr/lidarr.component"; import { MassEmailComponent } from "./massemail/massemail.component"; import { DiscordComponent } from "./notifications/discord.component"; import { EmailNotificationComponent } from "./notifications/emailnotification.component"; +import { GotifyComponent } from "./notifications/gotify.component"; import { MattermostComponent } from "./notifications/mattermost.component"; import { MobileComponent } from "./notifications/mobile.component"; import { NewsletterComponent } from "./notifications/newsletter.component"; @@ -48,6 +49,8 @@ import { WikiComponent } from "./wiki.component"; import { SettingsMenuComponent } from "./settingsmenu.component"; import { AutoCompleteModule, CalendarModule, DialogModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng"; +import { MatMenuModule} from "@angular/material"; +import { SharedModule } from "../shared/shared.module"; const routes: Routes = [ { path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] }, @@ -63,6 +66,7 @@ const routes: Routes = [ { path: "Slack", component: SlackComponent, canActivate: [AuthGuard] }, { path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] }, { path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] }, + { path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, { path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] }, { path: "Update", component: UpdateComponent, canActivate: [AuthGuard] }, @@ -99,6 +103,8 @@ const routes: Routes = [ PipeModule, RadioButtonModule, DialogModule, + SharedModule, + MatMenuModule ], declarations: [ SettingsMenuComponent, @@ -117,6 +123,7 @@ const routes: Routes = [ PushoverComponent, MattermostComponent, PushbulletComponent, + GotifyComponent, UserManagementComponent, UpdateComponent, AboutComponent, diff --git a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html index 91405d1c5..7685236ce 100644 --- a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html @@ -1,97 +1,61 @@ - - -
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/wiki.component.html b/src/Ombi/ClientApp/src/app/settings/wiki.component.html index d93fdb2b7..9f02b1354 100644 --- a/src/Ombi/ClientApp/src/app/settings/wiki.component.html +++ b/src/Ombi/ClientApp/src/app/settings/wiki.component.html @@ -1,15 +1,17 @@ 
- -
- - Wiki - + - \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/episode-request/episode-request.component.html b/src/Ombi/ClientApp/src/app/shared/episode-request/episode-request.component.html new file mode 100644 index 000000000..11962184e --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/episode-request/episode-request.component.html @@ -0,0 +1,83 @@ +
+ +
+
+ +
+
+ + + + +
+
+ +
+
+ + + + + Season {{season.seasonNumber}} + Season {{season.seasonNumber}} + + + Description + + + +
+
+
+ + +
+
+
+ {{ep.episodeNumber}} +
+
+ {{ep.title}} +
+
+ {{ep.airDate | amLocal | amDateFormat: 'L' }} +
+
+ {{ep.airDateDisplay }} +
+
+ Available + Processing + Request + Selected + Pending Approval + + Not Requested +
+ +
+ + +
+ + + +
+ + +
+
+
+
+ + + + +
+
diff --git a/src/Ombi/ClientApp/src/app/shared/episode-request/episode-request.component.ts b/src/Ombi/ClientApp/src/app/shared/episode-request/episode-request.component.ts new file mode 100644 index 000000000..c4ecb45fd --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/episode-request/episode-request.component.ts @@ -0,0 +1,105 @@ +import { Component, OnInit, Inject } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA, MatCheckboxChange } from "@angular/material"; +import { ISearchTvResultV2 } from "../../interfaces/ISearchTvResultV2"; +import { RequestService, MessageService } from "../../services"; +import { ITvRequestViewModel, ISeasonsViewModel, IEpisodesRequests, INewSeasonRequests } from "../../interfaces"; + + +@Component({ + selector: "episode-request", + templateUrl: "episode-request.component.html", +}) +export class EpisodeRequestComponent implements OnInit { + + public loading: boolean; + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public series: ISearchTvResultV2, + private requestService: RequestService, private notificationService: MessageService) { } + + public ngOnInit() { + this.loading = true; + + this.loading = false; + } + + public async submitRequests() { + // Make sure something has been selected + const selected = this.series.seasonRequests.some((season) => { + return season.episodes.some((ep) => { + return ep.selected; + }); + }); + debugger; + if (!selected && !this.series.requestAll && !this.series.firstSeason && !this.series.latestSeason) { + this.notificationService.send("You need to select some episodes!", "OK"); + return; + } + + this.series.requested = true; + + const viewModel = { firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id }; + viewModel.seasons = []; + this.series.seasonRequests.forEach((season) => { + const seasonsViewModel = { seasonNumber: season.seasonNumber, episodes: [] }; + if (!this.series.latestSeason && !this.series.requestAll && !this.series.firstSeason) { + season.episodes.forEach(ep => { + if (ep.selected) { + ep.requested = true; + seasonsViewModel.episodes.push({ episodeNumber: ep.episodeNumber }); + } + }); + } + viewModel.seasons.push(seasonsViewModel); + }); + + const requestResult = await this.requestService.requestTv(viewModel).toPromise(); + + if (requestResult.result) { + this.notificationService.send( + `Request for ${this.series.title} has been added successfully`); + + this.series.seasonRequests.forEach((season) => { + season.episodes.forEach((ep) => { + ep.selected = false; + }); + }); + + } else { + this.notificationService.send(requestResult.errorMessage ? requestResult.errorMessage : requestResult.message); + } + this.dialogRef.close(); + } + + public addRequest(episode: IEpisodesRequests) { + episode.selected = true; + } + + public removeRequest(episode: IEpisodesRequests) { + episode.selected = false; + } + + public seasonChanged(checkbox: MatCheckboxChange, season: INewSeasonRequests) { + season.episodes.forEach((ep) => { + if (checkbox.checked && (!ep.available && !ep.requested && !ep.approved)) { + this.addRequest(ep) + } else { + this.removeRequest(ep); + } + }); + } + + public async requestAllSeasons() { + this.series.requestAll = true; + await this.submitRequests(); + } + + public async requestFirstSeason() { + this.series.firstSeason = true; + await this.submitRequests(); + } + + public async requestLatestSeason() { + this.series.latestSeason = true; + await this.submitRequests(); + } +} diff --git a/src/Ombi/ClientApp/src/app/shared/functions/common-functions.ts b/src/Ombi/ClientApp/src/app/shared/functions/common-functions.ts new file mode 100644 index 000000000..8d94d8dc0 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/functions/common-functions.ts @@ -0,0 +1,28 @@ +export function getBaseLocation() { + debugger; + let paths: string[] = location.pathname.split('/').splice(1, 1); + let basePath: string = (paths && paths[0] ? paths[0] : ""); + if(invalidProxies.indexOf(basePath.toUpperCase()) === -1){ + return '/' + basePath; + } + return '/'; +} + +const invalidProxies: string[] = [ + 'DISCOVER', + 'REQUESTS-LIST', + 'SETTINGS', + 'ISSUES', + 'USERMANAGEMENT', + 'RECENTLYADDED', + 'DETAILS', + 'VOTE', + 'LOGIN', + 'LANDINGPAGE', + 'TOKEN', + 'RESET', + 'CUSTOM', + 'AUTH', + 'WIZARD', + "CALENDAR" +] \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index 985c95120..ad17ce1ed 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -10,12 +10,16 @@ import { IssuesReportComponent } from "./issues-report.component"; import { InputSwitchModule, SidebarModule } from "primeng/primeng"; import { - MatButtonModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatTooltipModule} from '@angular/material'; - import { MatCardModule, MatInputModule, MatTabsModule, MatAutocompleteModule, MatCheckboxModule, MatExpansionModule, MatDialogModule, MatSnackBarModule } from "@angular/material"; + MatButtonModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatTooltipModule, MatSelectModule, MatTableModule, MatPaginatorModule, MatSortModule, + MatTreeModule, MatStepperModule} from '@angular/material'; + import { MatCardModule, MatInputModule, MatTabsModule, MatAutocompleteModule, MatCheckboxModule, MatExpansionModule, MatDialogModule, MatProgressSpinnerModule, + MatChipsModule } from "@angular/material"; +import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; @NgModule({ declarations: [ IssuesReportComponent, + EpisodeRequestComponent, ], imports: [ SidebarModule, @@ -25,36 +29,54 @@ import { TruncateModule, MomentModule, MatCardModule, + MatProgressSpinnerModule, MatAutocompleteModule, MatInputModule, MatTabsModule, MatButtonModule, MatNativeDateModule, + MatChipsModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatCheckboxModule, + TranslateModule, MatExpansionModule, MatDialogModule, + MatTooltipModule, + MatSelectModule, + MatPaginatorModule, + MatSortModule, + MatTreeModule, + MatStepperModule, MatSnackBarModule, ], + entryComponents: [ + EpisodeRequestComponent + ], exports: [ TranslateModule, CommonModule, FormsModule, + TranslateModule, SidebarModule, + MatProgressSpinnerModule, IssuesReportComponent, + EpisodeRequestComponent, TruncateModule, InputSwitchModule, + MatTreeModule, MomentModule,MatCardModule, MatInputModule, MatTabsModule, + MatChipsModule, MatButtonModule, MatNativeDateModule, MatIconModule, MatSnackBarModule, MatSidenavModule, + MatSelectModule, MatListModule, MatToolbarModule, MatTooltipModule, @@ -62,6 +84,10 @@ import { MatCheckboxModule, MatExpansionModule, MatDialogModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatStepperModule, ], }) export class SharedModule {} diff --git a/src/Ombi/ClientApp/src/app/shared/storage/storage-service.ts b/src/Ombi/ClientApp/src/app/shared/storage/storage-service.ts new file mode 100644 index 000000000..297effd3a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/storage/storage-service.ts @@ -0,0 +1,18 @@ +import { Injectable } from "@angular/core"; + +@Injectable() +export class StorageService { + + public get(key: string): string { + return localStorage.getItem(key); + } + + public save(key: string, value: string): void { + this.remove(key); + localStorage.setItem(key, value); + } + + public remove(key: string) { + localStorage.removeItem(key); + } +} diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/index.ts b/src/Ombi/ClientApp/src/app/user-preferences/components/index.ts new file mode 100644 index 000000000..3dc080554 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/index.ts @@ -0,0 +1,12 @@ +import { AuthGuard } from "../../auth/auth.guard"; +import { Routes } from "@angular/router" +import { UserPreferenceComponent } from "./user-preference/user-preference.component"; + + +export const components: any[] = [ + UserPreferenceComponent, +]; + +export const routes: Routes = [ + { path: "", component: UserPreferenceComponent, canActivate: [AuthGuard] }, +]; \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html new file mode 100644 index 000000000..3f13f236c --- /dev/null +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.html @@ -0,0 +1,15 @@ +
+

+ +
+
+ + + + + {{lang.display}} + + + +
+
diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss new file mode 100644 index 000000000..b2bf4612b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.scss @@ -0,0 +1,5 @@ +.small-middle-container{ + margin: auto; + margin-top: 3%; + width: 80%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts new file mode 100644 index 000000000..a76df7149 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit } from "@angular/core"; +import { AuthService } from "../../../auth/auth.service"; +import { TranslateService } from "@ngx-translate/core"; +import { AvailableLanguages, ILanguage } from "./user-preference.constants"; +import { StorageService } from "../../../shared/storage/storage-service"; + +@Component({ + templateUrl: "./user-preference.component.html", + styleUrls: ["./user-preference.component.scss"], +}) +export class UserPreferenceComponent implements OnInit { + + public username: string; + public selectedLang: string; + public availableLanguages = AvailableLanguages; + + constructor(private authService: AuthService, + private readonly translate: TranslateService, + private storage: StorageService) { } + + public ngOnInit(): void { + const user = this.authService.claims(); + if(user.name) { + this.username = user.name; + } + const selectedLang = this.storage.get("Language"); + if(selectedLang) { + this.selectedLang = selectedLang; + } + } + + public languageSelected() { + this.storage.save("Language", this.selectedLang); + this.translate.use(this.selectedLang); + } + +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.constants.ts b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.constants.ts new file mode 100644 index 000000000..64b594c58 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/user-preferences/components/user-preference/user-preference.constants.ts @@ -0,0 +1,18 @@ +export const AvailableLanguages: ILanguage[] = [ + { display: 'English', value: 'en' }, + { display: 'Français', value: 'fr' }, + { display: 'Dansk', value: 'da' }, + { display: 'Deutsch', value: 'de' }, + { display: 'Italiano', value: 'it' }, + { display: 'Español', value: 'es' }, + { display: 'Nederlands', value: 'nl' }, + { display: 'Norsk', value: 'no' }, + { display: 'Português (Brasil)', value: 'pt' }, + { display: 'Polski', value: 'pl' }, + { display: 'Svenska', value: 'sv' }, +]; + +export interface ILanguage { + display: string; + value: string; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts b/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts new file mode 100644 index 000000000..32058c36b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/user-preferences/user-preferences.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router" + +import { SharedModule } from "../shared/shared.module"; + +import * as fromComponents from './components'; + + +@NgModule({ + imports: [ + RouterModule.forChild(fromComponents.routes), + SharedModule, + ], + declarations: [ + ...fromComponents.components + ], + exports: [ + RouterModule, + ], + providers: [ + ], + +}) +export class UserPreferencesModule { } diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html index 3f44a808b..8bc212c43 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html @@ -1,224 +1,175 @@ -

Create User

-

User: {{user.userName}}

+ +
- + + + User Details +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
- -
-