diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index 24c24d385..f7f2d4605 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -63,22 +63,11 @@ stages: $response = Invoke-WebRequest -Uri "https://ombireleasenote.azurewebsites.net/api/ReleaseNotesFunction?buildId=$(Build.BuildId)" Write-Host "##vso[task.setvariable variable=ReleaseNotes;]$response" - # - task: GitHubRelease@1 - # inputs: - # gitHubConnection: 'github.com_tidusjar' - # repositoryName: 'tidusjar/Ombi' - # action: 'create' - # target: '$(Build.SourceVersion)' - # tagSource: 'userSpecifiedTag' - # tag: '$(gitTag)' - # isDraft: true - # changeLogCompareToRelease: 'lastNonDraftRelease' - # changeLogType: 'commitBased' - - task: GitHubRelease@1 + displayName: 'Ombi.Releases Release' inputs: - gitHubConnection: 'github.com_tidusjar' - repositoryName: 'tidusjar/Ombi.Releases' + gitHubConnection: 'PAT' + repositoryName: 'Ombi-app/Ombi.Releases' action: 'create' target: 'c7fcbb77b58aef1076d635a9ef99e4374abc8672' tagSource: 'userSpecifiedTag' @@ -91,3 +80,38 @@ stages: isPreRelease: true changeLogCompareToRelease: 'lastNonDraftRelease' changeLogType: 'commitBased' + + - task: GitHubRelease@1 + displayName: 'Ombi Release' + inputs: + gitHubConnection: 'PAT' + repositoryName: 'Ombi-app/Ombi' + action: 'create' + target: '$(Build.SourceVersion)' + tagSource: 'userSpecifiedTag' + tag: '$(gitTag)' + releaseNotesSource: 'inline' + releaseNotesInline: '$(ReleaseNotes)' + assets: | + $(System.ArtifactsDirectory)/**/*.zip + $(System.ArtifactsDirectory)/**/*.tar.gz + isPreRelease: true + changeLogCompareToRelease: 'lastNonDraftRelease' + changeLogType: 'commitBased' + + - task: PowerShell@2 + displayName: "Trigger APT build" + inputs: + targetType: 'inline' + script: | + $body = @{ + "ref"="main" + "inputs"= @{"version"= "$(gitTag)"} + } | ConvertTo-Json + + $header = @{ + "Accept"="application/vnd.github.v3+json" + "Authorization"="Bearer ${env:APTPAT}" + } + + Invoke-RestMethod -Uri "https://api.github.com/repos/Ombi-app/Ombi.Apt/actions/workflows/build-deb.yml/dispatches" -Method 'Post' -Body $body -Headers $header \ No newline at end of file diff --git a/.azuredevops/pipelines/templates/variables.yml b/.azuredevops/pipelines/templates/variables.yml index c33cd7999..205e68eaa 100644 --- a/.azuredevops/pipelines/templates/variables.yml +++ b/.azuredevops/pipelines/templates/variables.yml @@ -27,4 +27,4 @@ variables: value: "4.0.$(Build.BuildId)" - name: isMain - value: $[eq(variables['Build.SourceBranch'], 'refs/heads/feature/v4')] \ No newline at end of file + value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8f206e648..dfe94f0f0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,7 +27,7 @@ If applicable, a snippet of the logs that seems relevant to the bug if present. - OS: [e.g. iOS] **Ombi Version (please complete the following information):** - - Version [e.g. 3.0.1158] + - Version [e.g. 4.0.958] - Media Server [e.g. Plex] - Database Type: SQLite (Please change if using MySQL) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 82f01f053..a9da96171 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Wiki - url: https://github.com/tidusjar/Ombi/wiki - about: The Ombi wiki should help guide you through installation and setup as well as help resolve common problems and answer frequently asked questions + - name: Docs + url: https://docs.ombi.app/ + about: The Ombi documentation should help guide you through installation and setup as well as help resolve common problems and answer frequently asked questions - name: Reddit support url: https://www.reddit.com/r/Ombi about: Ask questions about Ombi diff --git a/CHANGELOG.md b/CHANGELOG.md index 6febb6683..d4abf4412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -661,7 +661,7 @@ - Added capture of anonymous analytical data. [tidusjar] -- Added {AvailableDate} as a Notification Variable, this is the date the request was marked as available. See here: https://github.com/tidusjar/Ombi/wiki/Notification-Template-Variables. [tidusjar] +- Added {AvailableDate} as a Notification Variable, this is the date the request was marked as available. See here: https://docs.ombi.app/info/notification-template-variables/. [tidusjar] - Added the ability to search movies via the movie db with a different language! [tidusjar] diff --git a/README.md b/README.md index fdb90a5fc..d81c9a67d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,25 @@ + ![](http://i.imgur.com/qQsN78U.png) ____ [![Discord](https://img.shields.io/discord/270828201473736705.svg)](https://discord.gg/Sa7wNWb) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/ombi.svg)](https://hub.docker.com/r/linuxserver/ombi/) -[![Github All Releases](https://img.shields.io/github/downloads/tidusjar/Ombi/total.svg)](https://github.com/tidusjar/Ombi) +[![Github All Releases](https://img.shields.io/github/downloads/tidusjar/Ombi/total.svg)](https://github.com/ombi-app/Ombi) [![firsttimersonly](http://img.shields.io/badge/first--timers--only-friendly-blue.svg)](http://www.firsttimersonly.com/) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/ombi/localized.svg)](https://crowdin.com/project/ombi) [![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) +# Welcome + +Ombi is your friendly media request tool, automatically syncs with your media with your media servers! +Don't worry, it's grandma friendly, and more importantly; has wife approval certification 😂 + +| Service | Stable | Develop +|----------|:---------------------------:|:----------------------------:| +| Build Status | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build Status](https://dev.azure.com/tidusjar/Ombi/_apis/build/status/Ombi%20CI?repoName=Ombi-app%2FOmbi&branchName=develop)](https://dev.azure.com/tidusjar/Ombi/_build/latest?definitionId=18&repoName=Ombi-app%2FOmbi&branchName=develop) | [![Build Status](https://dev.azure.com/tidusjar/Ombi/_apis/build/status/Ombi%20CI?branchName=feature%2Fv4)](https://dev.azure.com/tidusjar/Ombi/_build/latest?definitionId=18&branchName=feature%2Fv4) +| Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/Ombi-app/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop/artifacts) | [![Download](http://i.imgur.com/odToka3.png)](https://github.com/ombi-app/ombi/releases) | + # Feature Requests Feature requests are handled on Feature Upvote. @@ -16,14 +27,13 @@ Search the existing requests to see if your suggestion has already been submitte (If a similar request exists, please vote, or add additional comments to the request) #### [![Feature Requests](https://cloud.githubusercontent.com/assets/390379/10127973/045b3a96-6560-11e5-9b20-31a2032956b2.png)](https://features.ombi.io) -___ -[![Twitter](https://img.shields.io/twitter/follow/tidusjar.svg?style=social)](https://twitter.com/intent/follow?screen_name=tidusjar) + -Follow me developing Ombi! + -[![Twitch](https://img.shields.io/badge/Twitch-Watch-blue.svg?style=flat-square&logo=twitch)](https://www.twitch.tv/tidusjar) + ___ @@ -31,23 +41,9 @@ ___
_**Note:** There is no longer an iOS app due to complications outside of our control._ -___ - -We also now have merch up on Teespring! -[EU Store](https://teespring.com/stores/ombi-eu) -[US Store](https://teespring.com/stores/ombi-us) - -___ - - -| Service | Stable | Develop | V4 | -|----------|:---------------------------:|:----------------------------:|:----------------------------:| -| Build Status | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/develop?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop) | [![Build Status](https://dev.azure.com/tidusjar/Ombi/_apis/build/status/Ombi%20CI?branchName=feature%2Fv4)](https://dev.azure.com/tidusjar/Ombi/_build/latest?definitionId=18&branchName=feature%2Fv4) -| Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop/artifacts) | [![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/ombi.releases/releases) | # Features -Here are some of the features Ombi V3 has: -* Now working without crashes on Linux. +Here are some of the features Ombi has: * Lets users request Movies, Music, and TV Shows (whether it being the entire series, an entire season, or even single episodes.) * Easily manage your requests * Allows you to set specific users to automatically have requests approved and added to the relevant service (Sonarr/Radarr/Lidarr/Couchpotato etc) @@ -58,72 +54,16 @@ Here are some of the features Ombi V3 has: * Will show if the request is already on plex or even if it's already monitored. * Automatically updates the status of requests when they are available on Plex/Emby * Slick, responsive and mobile friendly UI -* Ombi will automatically update itself :) (YMMV) -* Very fast! - -### Integration -We integrate with the following applications: -* Plex Media Server -* Emby -* Jellyfin -* Sonarr V2 and V3 -* Radarr V2 -* Lidarr -* DogNzb -* Couch Potato - - -### Notifications -Supported notifications: -* Mobile -* SMTP Notifications (Email) -* Discord -* Slack -* Pushbullet -* Pushover -* Mattermost -* Telegram -* Gotify -* Twilio -* Webhook - -### The difference between Version 4 and 3 - -Over the last year, we focused on the main functions on Ombi, a complete rewrite while making it better, faster and more stable. -We have already done most of the work, but some features are still be missing in this first version. -We are planning to bring back these features in V3 but for now you can find a list below with a quick comparison of features between v4 and v3. - - -| Service | Version 4 (Beta) | Version 3 (Stable)| -|----------|:----------:|:----------:| -| Multiple Plex/Emby/Jellyfin Servers | Yes | Yes | -| Emby/Jellyfin & Plex support | Yes | Yes | -| Mono dependency | No | No | -| Plex OAuth support | Yes | Yes | -| Login page | Yes (brand new) | Yes | -| Discovery page | Yes (brand new) | No | -| Request a movie collection | Yes (brand new) | No | -| Auto Delete Available Requests | Yes (brand new) | No | -| Report issues | Yes | Yes | -| Notifications support | Yes | Yes | -| Custom Notification Messages | Yes | Yes | -| Sending newsletters | Yes | Yes | -| Send a Mass Email | Yes | Yes | -| SickRage | Yes | Yes | -| CouchPotato | Yes | Yes | -| DogNzb | Yes | Yes | -| Headphones | No | Yes | -| Lidarr | Yes | Yes | + # Preview -![Preview](http://i.imgur.com/Nn1BwAM.gif) +![Preview](https://i.imgur.com/kBXIqer.png) # Installation -[Installation Guide](https://github.com/tidusjar/Ombi/wiki/Installation) -[Here for Reverse Proxy Config Examples](https://github.com/tidusjar/Ombi/wiki/Reverse-Proxy-Examples) -[PlexGuide.com - Ombi Deployment & 101 Demonstration!](https://www.youtube.com/watch?v=QPNlqqkjNJw&feature=youtu.be) +[Installation Guide](https://docs.ombi.app/installation/) +[Here for Reverse Proxy Config Examples](https://docs.ombi.app/info/reverse-proxy/) # Contributors @@ -139,9 +79,3 @@ If you feel like donating you can donate with the below buttons! [![Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/PlexRequestsNet) ### A massive thanks to everyone for all their help! - - -### Sponsors ### -- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools - - [ReSharper](http://www.jetbrains.com/resharper/) -- [BrowserStack](https://www.browserstack.com) for allowing us to use their platform for testing diff --git a/src/Ombi.Api.Emby/EmbyApiFactory.cs b/src/Ombi.Api.Emby/EmbyApiFactory.cs index c5c6e1c02..6f71938bf 100644 --- a/src/Ombi.Api.Emby/EmbyApiFactory.cs +++ b/src/Ombi.Api.Emby/EmbyApiFactory.cs @@ -25,10 +25,6 @@ namespace Ombi.Api.Emby public IEmbyApi CreateClient(EmbySettings settings) { - if (settings.IsJellyfin) - { - return new JellyfinApi(_api); - } return new EmbyApi(_api); } } diff --git a/src/Ombi.Api.Emby/Models/PublicInfo.cs b/src/Ombi.Api.Emby/Models/PublicInfo.cs index 01432d3c5..23db412b5 100644 --- a/src/Ombi.Api.Emby/Models/PublicInfo.cs +++ b/src/Ombi.Api.Emby/Models/PublicInfo.cs @@ -5,15 +5,8 @@ public string LocalAddress { get; set; } public string ServerName { get; set; } public string Version { get; set; } - /// - /// Only populated for Jellyfin - /// - public string ProductName { get; set; } - - public bool IsJellyfin => !string.IsNullOrEmpty(ProductName) && ProductName.Contains("Jellyfin"); - public string OperatingSystem { get; set; } public string Id { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs new file mode 100644 index 000000000..7a9ee8a5a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ombi.Api.Jellyfin.Models; +using Ombi.Api.Jellyfin.Models.Media.Tv; +using Ombi.Api.Jellyfin.Models.Movie; + +namespace Ombi.Api.Jellyfin +{ + public interface IBaseJellyfinApi + { + Task GetSystemInformation(string apiKey, string baseUrl); + Task> GetUsers(string baseUri, string apiKey); + Task LogIn(string username, string password, string apiKey, string baseUri); + + Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, + string baseUri); + + Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, + string baseUri); + + Task> GetAllShows(string apiKey, int startIndex, int count, string userId, + string baseUri); + + Task> GetCollection(string mediaId, + string apiKey, string userId, string baseUrl); + + Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetPublicInformation(string baseUrl); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/IJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs new file mode 100644 index 000000000..72d45877c --- /dev/null +++ b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ombi.Api.Jellyfin.Models; + +namespace Ombi.Api.Jellyfin +{ + public interface IJellyfinApi : IBaseJellyfinApi + { + Task LoginConnectUser(string username, string password); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Emby/JellyfinApi.cs b/src/Ombi.Api.Jellyfin/JellyfinApi.cs similarity index 66% rename from src/Ombi.Api.Emby/JellyfinApi.cs rename to src/Ombi.Api.Jellyfin/JellyfinApi.cs index 197fba684..a8d94eca6 100644 --- a/src/Ombi.Api.Emby/JellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/JellyfinApi.cs @@ -3,14 +3,14 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json; -using Ombi.Api.Emby.Models; -using Ombi.Api.Emby.Models.Media.Tv; -using Ombi.Api.Emby.Models.Movie; +using Ombi.Api.Jellyfin.Models; +using Ombi.Api.Jellyfin.Models.Media.Tv; +using Ombi.Api.Jellyfin.Models.Movie; using Ombi.Helpers; -namespace Ombi.Api.Emby +namespace Ombi.Api.Jellyfin { - public class JellyfinApi : IEmbyApi + public class JellyfinApi : IJellyfinApi { public JellyfinApi(IApi api) { @@ -20,27 +20,27 @@ namespace Ombi.Api.Emby private IApi Api { get; } /// - /// Returns all users from the Emby Instance + /// Returns all users from the Jellyfin Instance /// /// /// - public async Task> GetUsers(string baseUri, string apiKey) + public async Task> GetUsers(string baseUri, string apiKey) { var request = new Request("users", baseUri, HttpMethod.Get); AddHeaders(request, apiKey); - var obj = await Api.Request>(request); + var obj = await Api.Request>(request); return obj; } - public async Task GetSystemInformation(string apiKey, string baseUrl) + public async Task GetSystemInformation(string apiKey, string baseUrl) { var request = new Request("System/Info", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); - var obj = await Api.Request(request); + var obj = await Api.Request(request); return obj; } @@ -56,7 +56,7 @@ namespace Ombi.Api.Emby return obj; } - public async Task LogIn(string username, string password, string apiKey, string baseUri) + public async Task LogIn(string username, string password, string apiKey, string baseUri) { var request = new Request("users/authenticatebyname", baseUri, HttpMethod.Post); var body = new @@ -71,11 +71,11 @@ namespace Ombi.Api.Emby $"MediaBrowser Client=\"Ombi\", Device=\"Ombi\", DeviceId=\"v3\", Version=\"v3\""); AddHeaders(request, apiKey); - var obj = await Api.Request(request); + var obj = await Api.Request(request); return obj; } - public async Task> GetCollection(string mediaId, string apiKey, string userId, string baseUrl) + public async Task> GetCollection(string mediaId, string apiKey, string userId, string baseUrl) { var request = new Request($"users/{userId}/items?parentId={mediaId}", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); @@ -84,22 +84,22 @@ namespace Ombi.Api.Emby request.AddQueryString("IsVirtualItem", "False"); - return await Api.Request>(request); + return await Api.Request>(request); } - public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); + return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); } - public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); } - public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); } public async Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) @@ -126,7 +126,7 @@ namespace Ombi.Api.Emby return JsonConvert.DeserializeObject(response); } - private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview = false) + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview = false) { var request = new Request($"users/{userId}/items", baseUri, HttpMethod.Get); @@ -139,10 +139,10 @@ namespace Ombi.Api.Emby AddHeaders(request, apiKey); - var obj = await Api.Request>(request); + var obj = await Api.Request>(request); return obj; } - private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) { var request = new Request($"users/{userId}/items", baseUri, HttpMethod.Get); @@ -157,7 +157,7 @@ namespace Ombi.Api.Emby AddHeaders(request, apiKey); - var obj = await Api.Request>(request); + var obj = await Api.Request>(request); return obj; } @@ -172,7 +172,7 @@ namespace Ombi.Api.Emby req.AddHeader("Device", "Ombi"); } - public Task LoginConnectUser(string username, string password) + public Task LoginConnectUser(string username, string password) { throw new System.NotImplementedException(); } diff --git a/src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs b/src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs new file mode 100644 index 000000000..cc604b871 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs @@ -0,0 +1,37 @@ +using Ombi.Api; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using System.Threading.Tasks; + +namespace Ombi.Api.Jellyfin +{ + public class JellyfinApiFactory : IJellyfinApiFactory + { + private readonly ISettingsService _jellyfinSettings; + private readonly IApi _api; + + // TODO, if we need to derive futher, need to rework + public JellyfinApiFactory(ISettingsService jellyfinSettings, IApi api) + { + _jellyfinSettings = jellyfinSettings; + _api = api; + } + + public async Task CreateClient() + { + var settings = await _jellyfinSettings.GetSettingsAsync(); + return CreateClient(settings); + } + + public IJellyfinApi CreateClient(JellyfinSettings settings) + { + return new JellyfinApi(_api); + } + } + + public interface IJellyfinApiFactory + { + Task CreateClient(); + IJellyfinApi CreateClient(JellyfinSettings settings); + } +} diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs new file mode 100644 index 000000000..b7d2e39cb --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs @@ -0,0 +1,45 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinConfiguration.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinConfiguration + { + public bool PlayDefaultAudioTrack { get; set; } + public bool DisplayMissingEpisodes { get; set; } + public bool DisplayUnairedEpisodes { get; set; } + public object[] GroupedFolders { get; set; } + public string SubtitleMode { get; set; } + public bool DisplayCollectionsView { get; set; } + public bool EnableLocalPassword { get; set; } + public object[] OrderedViews { get; set; } + public object[] LatestItemsExcludes { get; set; } + public bool HidePlayedInLatest { get; set; } + public bool RememberAudioSelections { get; set; } + public bool RememberSubtitleSelections { get; set; } + public bool EnableNextEpisodeAutoPlay { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs new file mode 100644 index 000000000..a19418e3d --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs @@ -0,0 +1,47 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinConnectUser.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinConnectUser + { + public string AccessToken { get; set; } + public User User { get; set; } + } + + public class User + { + public string Id { get; set; } + public string Name { get; set; } + public string DisplayName { get; set; } + public string Email { get; set; } + public string IsActive { get; set; } + public string ImageUrl { get; set; } + public object IsSupporter { get; set; } + public object ExpDate { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs new file mode 100644 index 000000000..940d568fa --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinItemContainer + { + public List Items { get; set; } + public int TotalRecordCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs new file mode 100644 index 000000000..2c4f75be0 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs @@ -0,0 +1,10 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public enum JellyfinMediaType + { + Movie = 0, + Series = 1, + Music = 2, + Episode = 3 + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs new file mode 100644 index 000000000..8091b8b34 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs @@ -0,0 +1,59 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinPolicy.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinPolicy + { + public bool IsAdministrator { get; set; } + public bool IsHidden { get; set; } + public bool IsDisabled { get; set; } + public object[] BlockedTags { get; set; } + public bool EnableUserPreferenceAccess { get; set; } + public object[] AccessSchedules { get; set; } + public object[] BlockUnratedItems { get; set; } + public bool EnableRemoteControlOfOtherUsers { get; set; } + public bool EnableSharedDeviceControl { get; set; } + public bool EnableLiveTvManagement { get; set; } + public bool EnableLiveTvAccess { get; set; } + public bool EnableMediaPlayback { get; set; } + public bool EnableAudioPlaybackTranscoding { get; set; } + public bool EnableVideoPlaybackTranscoding { get; set; } + public bool EnablePlaybackRemuxing { get; set; } + public bool EnableContentDeletion { get; set; } + public bool EnableContentDownloading { get; set; } + public bool EnableSync { get; set; } + public bool EnableSyncTranscoding { get; set; } + public object[] EnabledDevices { get; set; } + public bool EnableAllDevices { get; set; } + public object[] EnabledChannels { get; set; } + public bool EnableAllChannels { get; set; } + public object[] EnabledFolders { get; set; } + public bool EnableAllFolders { get; set; } + public int InvalidLoginAttemptCount { get; set; } + public bool EnablePublicSharing { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs new file mode 100644 index 000000000..1cedee722 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs @@ -0,0 +1,63 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinSystemInfo.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinSystemInfo + { + public string SystemUpdateLevel { get; set; } + public string OperatingSystemDisplayName { get; set; } + public bool SupportsRunningAsService { get; set; } + public string MacAddress { get; set; } + public bool HasPendingRestart { get; set; } + public bool SupportsLibraryMonitor { get; set; } + public object[] InProgressInstallations { get; set; } + public int WebSocketPortNumber { get; set; } + public object[] CompletedInstallations { get; set; } + public bool CanSelfRestart { get; set; } + public bool CanSelfUpdate { get; set; } + public object[] FailedPluginAssemblies { get; set; } + public string ProgramDataPath { get; set; } + public string ItemsByNamePath { get; set; } + public string CachePath { get; set; } + public string LogPath { get; set; } + public string InternalMetadataPath { get; set; } + public string TranscodingTempPath { get; set; } + public int HttpServerPortNumber { get; set; } + public bool SupportsHttps { get; set; } + public int HttpsPortNumber { get; set; } + public bool HasUpdateAvailable { get; set; } + public bool SupportsAutoRunAtStartup { get; set; } + public string EncoderLocationType { get; set; } + public string SystemArchitecture { get; set; } + public string LocalAddress { get; set; } + public string WanAddress { get; set; } + public string ServerName { get; set; } + public string Version { get; set; } + public string OperatingSystem { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs new file mode 100644 index 000000000..51078f36c --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs @@ -0,0 +1,47 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinUser.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; + +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinUser + { + public string Name { get; set; } + public string ServerId { get; set; } + public string ConnectUserName { get; set; } + public string ConnectLinkType { get; set; } + public string Id { get; set; } + public bool HasPassword { get; set; } + public bool HasConfiguredPassword { get; set; } + public bool HasConfiguredEasyPassword { get; set; } + public DateTime LastLoginDate { get; set; } + public DateTime LastActivityDate { get; set; } + public JellyfinConfiguration Configuration { get; set; } + public JellyfinPolicy Policy { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs new file mode 100644 index 000000000..580cc45af --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs @@ -0,0 +1,7 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinUserLogin + { + public JellyfinUser User { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs new file mode 100644 index 000000000..332268ae3 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinChapter + { + public long StartPositionTicks { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs new file mode 100644 index 000000000..c53ec3cdf --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinExternalurl + { + public string Name { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs new file mode 100644 index 000000000..d2dcc0f8a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs @@ -0,0 +1,10 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinImagetags + { + public string Primary { get; set; } + public string Logo { get; set; } + public string Thumb { get; set; } + public string Banner { get; set; } + } +} diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs new file mode 100644 index 000000000..3cdec7c4c --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs @@ -0,0 +1,30 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinMediasource + { + public string Protocol { get; set; } + public string Id { get; set; } + public string Path { get; set; } + public string Type { get; set; } + public string Container { get; set; } + public string Name { get; set; } + public bool IsRemote { get; set; } + public string ETag { get; set; } + public long RunTimeTicks { get; set; } + public bool ReadAtNativeFramerate { get; set; } + public bool SupportsTranscoding { get; set; } + public bool SupportsDirectStream { get; set; } + public bool SupportsDirectPlay { get; set; } + public bool IsInfiniteStream { get; set; } + public bool RequiresOpening { get; set; } + public bool RequiresClosing { get; set; } + public bool SupportsProbing { get; set; } + public string VideoType { get; set; } + public JellyfinMediastream[] MediaStreams { get; set; } + public object[] PlayableStreamFileNames { get; set; } + public object[] Formats { get; set; } + public int Bitrate { get; set; } + public int DefaultAudioStreamIndex { get; set; } + + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs new file mode 100644 index 000000000..89da2651a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs @@ -0,0 +1,36 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinMediastream + { + public string Codec { get; set; } + public string Language { get; set; } + public string TimeBase { get; set; } + public string CodecTimeBase { get; set; } + public string NalLengthSize { get; set; } + public bool IsInterlaced { get; set; } + public bool IsAVC { get; set; } + public int BitRate { get; set; } + public int BitDepth { get; set; } + public int RefFrames { get; set; } + public bool IsDefault { get; set; } + public bool IsForced { get; set; } + public int Height { get; set; } + public int Width { get; set; } + public float AverageFrameRate { get; set; } + public float RealFrameRate { get; set; } + public string Profile { get; set; } + public string Type { get; set; } + public string AspectRatio { get; set; } + public int Index { get; set; } + public bool IsExternal { get; set; } + public bool IsTextSubtitleStream { get; set; } + public bool SupportsExternalStream { get; set; } + public string PixelFormat { get; set; } + public int Level { get; set; } + public bool IsAnamorphic { get; set; } + public string DisplayTitle { get; set; } + public string ChannelLayout { get; set; } + public int Channels { get; set; } + public int SampleRate { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs new file mode 100644 index 000000000..19bdd3f81 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs @@ -0,0 +1,11 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinPerson + { + public string Name { get; set; } + public string Id { get; set; } + public string Role { get; set; } + public string Type { get; set; } + public string PrimaryImageTag { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs new file mode 100644 index 000000000..9b47f9a1a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs @@ -0,0 +1,13 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinProviderids + { + public string Tmdb { get; set; } + public string Imdb { get; set; } + public string TmdbCollection { get; set; } + + public string Tvdb { get; set; } + public string Zap2It { get; set; } + public string TvRage { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs new file mode 100644 index 000000000..325ef8519 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinRemotetrailer + { + public string Url { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs new file mode 100644 index 000000000..c0d561102 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinStudio + { + public string Name { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs new file mode 100644 index 000000000..502c67822 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinUserdata + { + public double PlaybackPositionTicks { get; set; } + public int PlayCount { get; set; } + public bool IsFavorite { get; set; } + public bool Played { get; set; } + public string Key { get; set; } + public DateTime LastPlayedDate { get; set; } + public int UnplayedItemCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs b/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs new file mode 100644 index 000000000..d86bf5047 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs @@ -0,0 +1,34 @@ +using System; + +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinMovie + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Container { get; set; } + public DateTime PremiereDate { get; set; } + public object[] ProductionLocations { get; set; } + public string OfficialRating { get; set; } + public float CommunityRating { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string Type { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public bool HasSubtitles { get; set; } + public int CriticRating { get; set; } + public string Overview { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs b/src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs new file mode 100644 index 000000000..3dba49bea --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs @@ -0,0 +1,60 @@ +using System; + +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class MovieInformation + { + public string Name { get; set; } + public string OriginalTitle { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Etag { get; set; } + public DateTime DateCreated { get; set; } + public bool CanDelete { get; set; } + public bool CanDownload { get; set; } + public bool SupportsSync { get; set; } + public string Container { get; set; } + public string SortName { get; set; } + public DateTime PremiereDate { get; set; } + public JellyfinExternalurl[] ExternalUrls { get; set; } + public JellyfinMediasource[] MediaSources { get; set; } + public string[] ProductionLocations { get; set; } + public string Path { get; set; } + public string OfficialRating { get; set; } + public string Overview { get; set; } + public string[] Taglines { get; set; } + public string[] Genres { get; set; } + public float CommunityRating { get; set; } + public int VoteCount { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public JellyfinRemotetrailer[] RemoteTrailers { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string ParentId { get; set; } + public string Type { get; set; } + public JellyfinPerson[] People { get; set; } + public JellyfinStudio[] Studios { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string DisplayPreferencesId { get; set; } + public object[] Tags { get; set; } + public string[] Keywords { get; set; } + public JellyfinMediastream[] MediaStreams { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public object[] ScreenshotImageTags { get; set; } + public JellyfinChapter[] Chapters { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public string HomePageUrl { get; set; } + public int Budget { get; set; } + public float Revenue { get; set; } + public object[] LockedFields { get; set; } + public bool LockData { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs new file mode 100644 index 000000000..4df1e9bcc --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs @@ -0,0 +1,71 @@ +using System; +using Ombi.Api.Jellyfin.Models.Movie; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class EpisodeInformation + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Etag { get; set; } + public DateTime DateCreated { get; set; } + public bool CanDelete { get; set; } + public bool CanDownload { get; set; } + public bool SupportsSync { get; set; } + public string Container { get; set; } + public string SortName { get; set; } + public DateTime PremiereDate { get; set; } + public JellyfinExternalurl[] ExternalUrls { get; set; } + public JellyfinMediasource[] MediaSources { get; set; } + public string Path { get; set; } + public string Overview { get; set; } + public object[] Taglines { get; set; } + public object[] Genres { get; set; } + public string[] SeriesGenres { get; set; } + public float CommunityRating { get; set; } + public int VoteCount { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public int IndexNumber { get; set; } + public int ParentIndexNumber { get; set; } + public object[] RemoteTrailers { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string ParentId { get; set; } + public string Type { get; set; } + public object[] People { get; set; } + public object[] Studios { get; set; } + public string ParentLogoItemId { get; set; } + public string ParentBackdropItemId { get; set; } + public string[] ParentBackdropImageTags { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string SeriesName { get; set; } + public string SeriesId { get; set; } + public string SeasonId { get; set; } + public string DisplayPreferencesId { get; set; } + public object[] Tags { get; set; } + public object[] Keywords { get; set; } + public string SeriesPrimaryImageTag { get; set; } + public string SeasonName { get; set; } + public JellyfinMediastream[] MediaStreams { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public object[] BackdropImageTags { get; set; } + public object[] ScreenshotImageTags { get; set; } + public string ParentLogoImageTag { get; set; } + public string SeriesStudio { get; set; } + public JellyfinSeriesstudioinfo SeriesStudioInfo { get; set; } + public string ParentThumbItemId { get; set; } + public string ParentThumbImageTag { get; set; } + public JellyfinChapter[] Chapters { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public object[] LockedFields { get; set; } + public bool LockData { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs new file mode 100644 index 000000000..a2769138e --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs @@ -0,0 +1,45 @@ +using Ombi.Api.Jellyfin.Models.Movie; +using System; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinEpisodes + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Container { get; set; } + public DateTime PremiereDate { get; set; } + public float CommunityRating { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public int IndexNumber { get; set; } + public int? IndexNumberEnd { get; set; } + public int ParentIndexNumber { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string Type { get; set; } + public string ParentLogoItemId { get; set; } + public string ParentBackdropItemId { get; set; } + public string[] ParentBackdropImageTags { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string SeriesName { get; set; } + public string SeriesId { get; set; } + public string SeasonId { get; set; } + public string SeriesPrimaryImageTag { get; set; } + public string SeasonName { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public object[] BackdropImageTags { get; set; } + public string ParentLogoImageTag { get; set; } + public string ParentThumbItemId { get; set; } + public string ParentThumbImageTag { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public bool HasSubtitles { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs new file mode 100644 index 000000000..663dc4ce3 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinRemotetrailer + { + public string Url { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs new file mode 100644 index 000000000..0d0fb21be --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs @@ -0,0 +1,32 @@ +using Ombi.Api.Jellyfin.Models.Movie; +using System; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinSeries + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public DateTime PremiereDate { get; set; } + public string OfficialRating { get; set; } + public float CommunityRating { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsFolder { get; set; } + public string Type { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public int ChildCount { get; set; } + public string Status { get; set; } + public string AirTime { get; set; } + public string[] AirDays { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public string LocationType { get; set; } + public DateTime EndDate { get; set; } + + public JellyfinProviderids ProviderIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs new file mode 100644 index 000000000..ada6be68d --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinSeriesstudioinfo + { + public string Name { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs new file mode 100644 index 000000000..efb48560e --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs @@ -0,0 +1,59 @@ +using System; +using Ombi.Api.Jellyfin.Models.Movie; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class SeriesInformation + { + + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Etag { get; set; } + public DateTime DateCreated { get; set; } + public DateTime DateLastMediaAdded { get; set; } + public bool CanDelete { get; set; } + public bool CanDownload { get; set; } + public bool SupportsSync { get; set; } + public string SortName { get; set; } + public DateTime PremiereDate { get; set; } + public JellyfinExternalurl[] ExternalUrls { get; set; } + public string Path { get; set; } + public string OfficialRating { get; set; } + public string Overview { get; set; } + public string ShortOverview { get; set; } + public object[] Taglines { get; set; } + public string[] Genres { get; set; } + public float CommunityRating { get; set; } + public int VoteCount { get; set; } + public long CumulativeRunTimeTicks { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public JellyfinRemotetrailer[] RemoteTrailers { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + public bool IsFolder { get; set; } + public string ParentId { get; set; } + public string Type { get; set; } + public JellyfinPerson[] People { get; set; } + public JellyfinStudio[] Studios { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public int RecursiveItemCount { get; set; } + public int ChildCount { get; set; } + public string DisplayPreferencesId { get; set; } + public string Status { get; set; } + public string AirTime { get; set; } + public string[] AirDays { get; set; } + public object[] Tags { get; set; } + public object[] Keywords { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public object[] ScreenshotImageTags { get; set; } + public string LocationType { get; set; } + public string HomePageUrl { get; set; } + public object[] LockedFields { get; set; } + public bool LockData { get; set; } + + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs b/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs new file mode 100644 index 000000000..6687cf3c9 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs @@ -0,0 +1,12 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public class PublicInfo + { + public string LocalAddress { get; set; } + public string ServerName { get; set; } + public string Version { get; set; } + public string OperatingSystem { get; set; } + public string Id { get; set; } + } + +} diff --git a/src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj b/src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj new file mode 100644 index 000000000..5457b0290 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + 3.0.0.0 + 3.0.0.0 + + + 8.0 + + + + + + + + \ No newline at end of file diff --git a/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs b/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs index 69d8231ad..3b47cdd28 100644 --- a/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs +++ b/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Hqub.MusicBrainz.API.Entities; +using Hqub.MusicBrainz.API.Entities.Collections; using Ombi.Api.MusicBrainz.Models; namespace Ombi.Api.MusicBrainz @@ -11,6 +12,7 @@ namespace Ombi.Api.MusicBrainz Task> SearchArtist(string artistQuery); Task> GetReleaseForArtist(string artistId); Task GetArtistInformation(string artistId); + Task GetAlbumInformation(string albumId); Task GetCoverArtForReleaseGroup(string musicBrainzId, CancellationToken token); } } \ No newline at end of file diff --git a/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs b/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs index 3c86d7882..e870eb07f 100644 --- a/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs +++ b/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Hqub.MusicBrainz.API; using Hqub.MusicBrainz.API.Entities; +using Hqub.MusicBrainz.API.Entities.Collections; using Newtonsoft.Json; using Ombi.Api.MusicBrainz.Models; @@ -20,6 +21,12 @@ namespace Ombi.Api.MusicBrainz _api = api; } + public Task GetAlbumInformation(string albumId) + { + var album = Release.GetAsync(albumId); + return album; + } + public async Task> SearchArtist(string artistQuery) { var artist = await Artist.SearchAsync(artistQuery, 10); diff --git a/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs b/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs index 6877708c5..699b37b3c 100644 --- a/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs +++ b/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs @@ -3,5 +3,6 @@ public class SystemStatus { public string version { get; set; } + public string urlBase { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Api.RottenTomatoes/IRottenTomatoesApi.cs b/src/Ombi.Api.RottenTomatoes/IRottenTomatoesApi.cs new file mode 100644 index 000000000..4466832b1 --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/IRottenTomatoesApi.cs @@ -0,0 +1,14 @@ +using Ombi.Api.RottenTomatoes.Models; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api.RottenTomatoes +{ + public interface IRottenTomatoesApi + { + Task GetMovieRatings(string movieName, int movieYear); + Task GetTvRatings(string showName, int showYear); + } +} diff --git a/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesMovieResponse.cs b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesMovieResponse.cs new file mode 100644 index 000000000..ceea5000b --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesMovieResponse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace Ombi.Api.RottenTomatoes.Models +{ + public class RottenTomatoesMovieResponse + { + public int total { get; set; } + public List movies { get; set; } + } + + public class Movie + { + public string id { get; set; } + public string title { get; set; } + public int year { get; set; } + public string mpaa_rating { get; set; } + public object runtime { get; set; } + public string critics_consensus { get; set; } + public MovieRatings ratings { get; set; } + public Links links { get; set; } + } + + public class MovieRatings + { + public string critics_rating { get; set; } + public int critics_score { get; set; } + public string audience_rating { get; set; } + public int audience_score { get; set; } + } + + public class Links + { + public string alternate { get; set; } + } +} diff --git a/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesTvResponse.cs b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesTvResponse.cs new file mode 100644 index 000000000..234e958be --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesTvResponse.cs @@ -0,0 +1,20 @@ +namespace Ombi.Api.RottenTomatoes.Models +{ + public class RottenTomatoesTvResponse + { + public int tvCount { get; set; } + public TvSeries[] tvSeries { get; set; } + } + + public class TvSeries + { + public string title { get; set; } + public int startYear { get; set; } + public int endYear { get; set; } + public string url { get; set; } + public string meterClass { get; set; } + public int meterScore { get; set; } + public string image { get; set; } + } + +} diff --git a/src/Ombi.Api.RottenTomatoes/Models/TvRatings.cs b/src/Ombi.Api.RottenTomatoes/Models/TvRatings.cs new file mode 100644 index 000000000..902d532d7 --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Models/TvRatings.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.RottenTomatoes.Models +{ + public class TvRatings + { + public string Class { get; set; } + public int Score { get; set; } + } +} diff --git a/src/Ombi.Api.RottenTomatoes/Ombi.Api.RottenTomatoes.csproj b/src/Ombi.Api.RottenTomatoes/Ombi.Api.RottenTomatoes.csproj new file mode 100644 index 000000000..8cb4799bc --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Ombi.Api.RottenTomatoes.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + 8.0 + + + + + + + diff --git a/src/Ombi.Api.RottenTomatoes/RottenTomatoesApi.cs b/src/Ombi.Api.RottenTomatoes/RottenTomatoesApi.cs new file mode 100644 index 000000000..88dfa2f79 --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/RottenTomatoesApi.cs @@ -0,0 +1,56 @@ +using Ombi.Api.RottenTomatoes.Models; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api.RottenTomatoes +{ + public class RottenTomatoesApi : IRottenTomatoesApi + { + public RottenTomatoesApi(IApi api) + { + _api = api; + } + + private string Endpoint => "https://www.rottentomatoes.com/api/private"; + private IApi _api { get; } + + public async Task GetMovieRatings(string movieName, int movieYear) + { + var request = new Request("/v1.0/movies", Endpoint, HttpMethod.Get); + request.AddHeader("Accept", "application/json"); + request.AddQueryString("q", movieName); + var result = await _api.Request(request); + + var movieFound = result.movies.FirstOrDefault(x => x.year == movieYear); + if (movieFound == null) + { + return null; + } + + return movieFound.ratings; + } + + public async Task GetTvRatings(string showName, int showYear) + { + var request = new Request("/v2.0/search/", Endpoint, HttpMethod.Get); + request.AddHeader("Accept", "application/json"); + request.AddQueryString("q", showName); + request.AddQueryString("limit", 10.ToString()); + var result = await _api.Request(request); + + var showFound = result.tvSeries.FirstOrDefault(x => x.startYear == showYear); + if (showFound == null) + { + return null; + } + + return new TvRatings + { + Class = showFound.meterClass, + Score = showFound.meterScore + }; + } + } +} diff --git a/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs b/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs index bd6e8d5a2..f4e6f59a3 100644 --- a/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs +++ b/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs @@ -30,7 +30,7 @@ namespace Ombi.Core.Tests.Authentication 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); + PlexApi.Object, null, null, null, null, AuthenticationSettings.Object); } public OmbiUserManager _um { get; set; } diff --git a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs index 8e5c57d67..132f51e49 100644 --- a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs @@ -115,4 +115,4 @@ namespace Ombi.Core.Tests.Rule.Search Assert.False(search.Available); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs new file mode 100644 index 000000000..1b838f102 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +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; + +namespace Ombi.Core.Tests.Rule.Search +{ + public class JellyfinAvailabilityRuleTests + { + [SetUp] + public void Setup() + { + ContextMock = new Mock(); + SettingsMock = new Mock>(); + Rule = new JellyfinAvailabilityRule(ContextMock.Object, SettingsMock.Object); + } + + private JellyfinAvailabilityRule Rule { get; set; } + private Mock ContextMock { get; set; } + private Mock> SettingsMock { get; set; } + + [Test] + public async Task Movie_ShouldBe_Available_WhenFoundInJellyfin() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new JellyfinSettings()); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new JellyfinContent + { + ProviderId = "123" + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.True(search.Available); + } + + [Test] + public async Task Movie_Has_Custom_Url_When_Specified_In_Settings() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new JellyfinSettings + { + Enable = true, + Servers = new List + { + new JellyfinServers + { + ServerHostname = "http://test.com/", + ServerId = "8" + } + } + }); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new JellyfinContent + { + ProviderId = "123", + JellyfinId = 1.ToString(), + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.That(search.JellyfinUrl, Is.EqualTo("http://test.com/web/index.html#!/details?id=1&serverId=8")); + } + + [Test] + public async Task Movie_Uses_Default_Url_When() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new JellyfinSettings + { + Enable = true, + Servers = new List + { + new JellyfinServers + { + Ip = "8080", + Port = 9090, + ServerHostname = string.Empty, + ServerId = "8" + } + } + }); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new JellyfinContent + { + ProviderId = "123", + JellyfinId = 1.ToString() + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + } + + [Test] + public async Task Movie_ShouldBe_NotAvailable_WhenNotFoundInJellyfin() + { + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).Returns(Task.FromResult(default(JellyfinContent))); + var search = new SearchMovieViewModel(); + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.False(search.Available); + } + } +} diff --git a/src/Ombi.Core.Tests/WatchProviderParserTests.cs b/src/Ombi.Core.Tests/WatchProviderParserTests.cs new file mode 100644 index 000000000..c56f64946 --- /dev/null +++ b/src/Ombi.Core.Tests/WatchProviderParserTests.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Core.Helpers; +using Ombi.Store.Entities; +using System.Collections.Generic; + +namespace Ombi.Core.Tests +{ + [TestFixture] + public class WatchProviderParserTests + { + [TestCase("GB", TestName = "UpperCase")] + [TestCase("gb", TestName = "LowerCase")] + [TestCase("gB", TestName = "MixedCase")] + public void GetValidStreamData(string streamingCountry) + { + var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders + { + Results = new Results + { + GB = new WatchProviderData() + { + StreamInformation = new List + { + new StreamData + { + provider_name = "Netflix", + display_priority = 0, + logo_path = "logo", + provider_id = 8 + } + } + } + } + }, new OmbiUser { StreamingCountry = streamingCountry }); + + Assert.That(result[0].provider_name, Is.EqualTo("Netflix")); + } + + [TestCase("GB", TestName = "Missing_UpperCase")] + [TestCase("gb", TestName = "Missing_LowerCase")] + [TestCase("gB", TestName = "Missing_MixedCase")] + public void GetMissingStreamData(string streamingCountry) + { + var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders + { + Results = new Results + { + AR = new WatchProviderData() + { + StreamInformation = new List + { + new StreamData + { + provider_name = "Netflix", + display_priority = 0, + logo_path = "logo", + provider_id = 8 + } + } + } + } + }, new OmbiUser { StreamingCountry = streamingCountry }); + + Assert.That(result, Is.Empty); + } + + [Test] + public void GetInvalidStreamData() + { + var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders + { + Results = new Results + { + AR = new WatchProviderData() + { + StreamInformation = new List + { + new StreamData + { + provider_name = "Netflix", + display_priority = 0, + logo_path = "logo", + provider_id = 8 + } + } + } + } + }, new OmbiUser { StreamingCountry = "BLAH" }); + + Assert.That(result, Is.Empty); + } + } +} diff --git a/src/Ombi.Core/Authentication/OmbiUserManager.cs b/src/Ombi.Core/Authentication/OmbiUserManager.cs index 8313f359b..87f82c1de 100644 --- a/src/Ombi.Core/Authentication/OmbiUserManager.cs +++ b/src/Ombi.Core/Authentication/OmbiUserManager.cs @@ -33,6 +33,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; using Ombi.Core.Settings; @@ -49,18 +50,24 @@ namespace Ombi.Core.Authentication IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IPlexApi plexApi, - IEmbyApiFactory embyApi, ISettingsService embySettings, ISettingsService auth) + IEmbyApiFactory embyApi, ISettingsService embySettings, + IJellyfinApiFactory jellyfinApi, ISettingsService jellyfinSettings, + ISettingsService auth) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { _plexApi = plexApi; _embyApi = embyApi; + _jellyfinApi = jellyfinApi; _embySettings = embySettings; + _jellyfinSettings = jellyfinSettings; _authSettings = auth; } private readonly IPlexApi _plexApi; private readonly IEmbyApiFactory _embyApi; + private readonly IJellyfinApiFactory _jellyfinApi; private readonly ISettingsService _embySettings; + private readonly ISettingsService _jellyfinSettings; private readonly ISettingsService _authSettings; public override async Task CheckPasswordAsync(OmbiUser user, string password) @@ -83,6 +90,10 @@ namespace Ombi.Core.Authentication { return await CheckEmbyPasswordAsync(user, password); } + if (user.UserType == UserType.JellyfinUser) + { + return await CheckJellyfinPasswordAsync(user, password); + } return false; } @@ -185,5 +196,36 @@ namespace Ombi.Core.Authentication } return false; } + + /// + /// Sign the user into Jellyfin + /// We do not check if the user is in the owners "friends" since they must have a local user account to get this far. + /// We also have to try and authenticate them with every server, the first server that work we just say it was a success + /// + /// + /// + /// + private async Task CheckJellyfinPasswordAsync(OmbiUser user, string password) + { + var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync(); + var client = _jellyfinApi.CreateClient(jellyfinSettings); + + foreach (var server in jellyfinSettings.Servers) + { + try + { + var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri); + if (result != null) + { + return true; + } + } + catch (Exception e) + { + Logger.LogError(e, "Jellyfin Login Failed"); + } + } + return false; + } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Engine/BaseMediaEngine.cs b/src/Ombi.Core/Engine/BaseMediaEngine.cs index fc9847c7d..66e60767a 100644 --- a/src/Ombi.Core/Engine/BaseMediaEngine.cs +++ b/src/Ombi.Core/Engine/BaseMediaEngine.cs @@ -15,6 +15,8 @@ using Ombi.Core.Settings; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; using Ombi.Store.Repository; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Core.Helpers; namespace Ombi.Core.Engine { @@ -179,6 +181,12 @@ namespace Ombi.Core.Engine return user.Language; } + protected async Task> GetUserWatchProvider(WatchProviders providers) + { + var user = await GetUser(); + return WatchProviderParser.GetUserWatchProviders(providers, user); + } + private OmbiSettings ombiSettings; protected async Task GetOmbiSettings() { diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 3b8d97dc0..746045ef3 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -26,5 +26,6 @@ namespace Ombi.Core.Engine.Interfaces int ResultLimit { get; set; } Task GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted); + Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs index f9889ec9c..efd42dc68 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs @@ -9,5 +9,6 @@ namespace Ombi.Core.Engine.Interfaces Task GetArtistInformation(string artistId); Task GetArtistInformationByRequestId(int requestId); Task GetReleaseGroupArt(string musicBrainzId, CancellationToken token); + Task GetAlbum(string albumId); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs index a8a27aa19..d2201825f 100644 --- a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Ombi.Core.Models.Search.V2; namespace Ombi.Core @@ -7,5 +9,6 @@ namespace Ombi.Core { Task GetShowInformation(int tvdbid); Task GetShowByRequest(int requestId); + Task> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 741503795..c9dbde067 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -67,6 +67,22 @@ namespace Ombi.Core.Engine $"{movieInfo.Title}{(!string.IsNullOrEmpty(movieInfo.ReleaseDate) ? $" ({DateTime.Parse(movieInfo.ReleaseDate).Year})" : string.Empty)}"; var userDetails = await GetUser(); + var canRequestOnBehalf = false; + + if (model.RequestOnBehalf.HasValue()) + { + canRequestOnBehalf = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); + + if (!canRequestOnBehalf) + { + return new RequestEngineResult + { + Result = false, + Message = "You do not have the correct permissions to request on behalf of users!", + ErrorMessage = $"You do not have the correct permissions to request on behalf of users!" + }; + } + } var requestModel = new MovieRequests { @@ -82,7 +98,7 @@ namespace Ombi.Core.Engine Status = movieInfo.Status, RequestedDate = DateTime.UtcNow, Approved = false, - RequestedUserId = userDetails.Id, + RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id, Background = movieInfo.BackdropPath, LangCode = model.LanguageCode, RequestedByAlias = model.RequestedByAlias @@ -103,7 +119,7 @@ namespace Ombi.Core.Engine if (requestModel.Approved) // The rules have auto approved this { - var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName); + var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf); if (requestEngineResult.Result) { var result = await ApproveMovie(requestModel); @@ -124,7 +140,7 @@ namespace Ombi.Core.Engine // If there are no providers then it's successful but movie has not been sent } - return await AddMovieRequest(requestModel, fullMovieName); + return await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf); } @@ -270,7 +286,7 @@ namespace Ombi.Core.Engine allRequests = allRequests.Where(x => x.Available); break; case RequestStatus.Denied: - allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available); + allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available); break; default: break; @@ -429,7 +445,7 @@ namespace Ombi.Core.Engine public async Task GetRequest(int requestId) { var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync(); - await CheckForSubscription(new HideResult(), new List{request }); + await CheckForSubscription(new HideResult(), new List { request }); return request; } @@ -654,19 +670,19 @@ namespace Ombi.Core.Engine }; } - private async Task AddMovieRequest(MovieRequests model, string movieName) + private async Task AddMovieRequest(MovieRequests model, string movieName, string requestOnBehalf) { await MovieRepository.Add(model); var result = await RunSpecificRule(model, SpecificRules.CanSendNotification); if (result.Success) - { + { await NotificationHelper.NewRequest(model); } await _requestLog.Add(new RequestLog { - UserId = (await GetUser()).Id, + UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, RequestDate = DateTime.UtcNow, RequestId = model.Id, RequestType = RequestType.Movie, diff --git a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs index 114d32a24..d597ec80b 100644 --- a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -13,38 +13,45 @@ namespace Ombi.Core.Engine { public class RecentlyAddedEngine : IRecentlyAddedEngine { - public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository recentlyAdded) + public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IJellyfinContentRepository jellyfin, IRepository recentlyAdded) { _plex = plex; _emby = emby; + _jellyfin = jellyfin; _recentlyAddedLog = recentlyAdded; } private readonly IPlexContentRepository _plex; private readonly IEmbyContentRepository _emby; + private readonly IJellyfinContentRepository _jellyfin; private readonly IRepository _recentlyAddedLog; + + public IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to) { var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt > from && x.AddedAt < to); var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt > from && x.AddedAt < to); + var jellyfinMovies = _jellyfin.GetAll().Where(x => x.Type == JellyfinMediaType.Movie && x.AddedAt > from && x.AddedAt < to); - return GetRecentlyAddedMovies(plexMovies, embyMovies).Take(30); + return GetRecentlyAddedMovies(plexMovies, embyMovies, jellyfinMovies).Take(30); } public IEnumerable GetRecentlyAddedMovies() { var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie); var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie); - return GetRecentlyAddedMovies(plexMovies, embyMovies); + var jellyfinMovies = _jellyfin.GetAll().Where(x => x.Type == JellyfinMediaType.Movie); + return GetRecentlyAddedMovies(plexMovies, embyMovies, jellyfinMovies); } public IEnumerable GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason) { var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show && x.AddedAt > from && x.AddedAt < to); var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series && x.AddedAt > from && x.AddedAt < to); + var jellyfinTv = _jellyfin.GetAll().Include(x => x.Episodes).Where(x => x.Type == JellyfinMediaType.Series && x.AddedAt > from && x.AddedAt < to); - return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason).Take(30); + return GetRecentlyAddedTv(plexTv, embyTv, jellyfinTv, groupBySeason).Take(30); } @@ -52,14 +59,16 @@ namespace Ombi.Core.Engine { var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show); var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series); + var jellyfinTv = _jellyfin.GetAll().Include(x => x.Episodes).Where(x => x.Type == JellyfinMediaType.Series); - return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason); + return GetRecentlyAddedTv(plexTv, embyTv, jellyfinTv, groupBySeason); } public async Task UpdateRecentlyAddedDatabase() { var plexContent = _plex.GetAll().Include(x => x.Episodes); var embyContent = _emby.GetAll().Include(x => x.Episodes); + var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes); var recentlyAddedLog = new HashSet(); foreach (var p in plexContent) { @@ -136,17 +145,56 @@ namespace Ombi.Core.Engine } } } + + foreach (var e in jellyfinContent) + { + if (e.TheMovieDbId.IsNullOrEmpty()) + { + continue; + } + if (e.Type == JellyfinMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Jellyfin, + ContentId = int.Parse(e.TheMovieDbId), + ContentType = ContentType.Parent + }); + } + else + { + // Add the episodes + foreach (var ep in e.Episodes) + { + if (ep.Series.TvDbId.IsNullOrEmpty()) + { + continue; + } + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Jellyfin, + ContentId = int.Parse(ep.Series.TvDbId), + ContentType = ContentType.Episode, + EpisodeNumber = ep.EpisodeNumber, + SeasonNumber = ep.SeasonNumber + }); + } + } + } await _recentlyAddedLog.AddRange(recentlyAddedLog); return true; } - private IEnumerable GetRecentlyAddedTv(IQueryable plexTv, IQueryable embyTv, + private IEnumerable GetRecentlyAddedTv(IQueryable plexTv, IQueryable embyTv, IQueryable jellyfinTv, bool groupBySeason) { var model = new HashSet(); TransformPlexShows(plexTv, model); TransformEmbyShows(embyTv, model); + TransformJellyfinShows(jellyfinTv, model); if (groupBySeason) { @@ -156,11 +204,12 @@ namespace Ombi.Core.Engine return model; } - private IEnumerable GetRecentlyAddedMovies(IQueryable plexMovies, IQueryable embyMovies) + private IEnumerable GetRecentlyAddedMovies(IQueryable plexMovies, IQueryable embyMovies, IQueryable jellyfinMovies) { var model = new HashSet(); TransformPlexMovies(plexMovies, model); TransformEmbyMovies(embyMovies, model); + TransformJellyfinMovies(jellyfinMovies, model); return model; } @@ -181,6 +230,22 @@ namespace Ombi.Core.Engine } } + private static void TransformJellyfinMovies(IQueryable jellyfinMovies, HashSet model) + { + foreach (var jellyfin in jellyfinMovies) + { + model.Add(new RecentlyAddedMovieModel + { + Id = jellyfin.Id, + ImdbId = jellyfin.ImdbId, + TheMovieDbId = jellyfin.TheMovieDbId, + TvDbId = jellyfin.TvDbId, + AddedAt = jellyfin.AddedAt, + Title = jellyfin.Title, + }); + } + } + private static void TransformPlexMovies(IQueryable plexMovies, HashSet model) { foreach (var plex in plexMovies) @@ -244,5 +309,26 @@ namespace Ombi.Core.Engine } } } + + private static void TransformJellyfinShows(IQueryable jellyfinShows, HashSet model) + { + foreach (var jellyfin in jellyfinShows) + { + foreach (var episode in jellyfin.Episodes) + { + model.Add(new RecentlyAddedTvModel + { + Id = jellyfin.Id, + ImdbId = jellyfin.ImdbId, + TvDbId = jellyfin.TvDbId, + TheMovieDbId = jellyfin.TheMovieDbId, + AddedAt = jellyfin.AddedAt, + Title = jellyfin.Title, + EpisodeNumber = episode.EpisodeNumber, + SeasonNumber = episode.SeasonNumber + }); + } + } + } } } diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index e066261f4..b06c99d49 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -51,12 +51,28 @@ namespace Ombi.Core.Engine public async Task RequestTvShow(TvRequestViewModel tv) { var user = await GetUser(); + var canRequestOnBehalf = false; + + if (tv.RequestOnBehalf.HasValue()) + { + canRequestOnBehalf = await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, OmbiRoles.Admin); + + if (!canRequestOnBehalf) + { + return new RequestEngineResult + { + Result = false, + Message = "You do not have the correct permissions to request on behalf of users!", + ErrorMessage = $"You do not have the correct permissions to request on behalf of users!" + }; + } + } var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi); (await tvBuilder .GetShowInfo(tv.TvDbId)) .CreateTvList(tv) - .CreateChild(tv, user.Id); + .CreateChild(tv, canRequestOnBehalf ? tv.RequestOnBehalf : user.Id); await tvBuilder.BuildEpisodes(tv); @@ -124,12 +140,12 @@ namespace Ombi.Core.Engine ErrorMessage = "This has already been requested" }; } - return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest); + return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf); } // This is a new request var newRequest = tvBuilder.CreateNewRequest(tv); - return await AddRequest(newRequest.NewRequest); + return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf); } public async Task> GetRequests(int count, int position, OrderFilterModel type) @@ -736,21 +752,21 @@ namespace Ombi.Core.Engine } } - private async Task AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest) + private async Task AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf) { // Add the child existingRequest.ChildRequests.Add(newRequest); await TvRepository.Update(existingRequest); - return await AfterRequest(newRequest); + return await AfterRequest(newRequest, requestOnBehalf); } - private async Task AddRequest(TvRequests model) + private async Task AddRequest(TvRequests model, string requestOnBehalf) { await TvRepository.Add(model); // This is a new request so we should only have 1 child - return await AfterRequest(model.ChildRequests.FirstOrDefault()); + return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf); } private static List SortEpisodes(List items) @@ -766,7 +782,7 @@ namespace Ombi.Core.Engine } - private async Task AfterRequest(ChildRequests model) + private async Task AfterRequest(ChildRequests model, string requestOnBehalf) { var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification); if (sendRuleResult.Success) @@ -776,7 +792,7 @@ namespace Ombi.Core.Engine await _requestLog.Add(new RequestLog { - UserId = (await GetUser()).Id, + UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, RequestDate = DateTime.UtcNow, RequestId = model.Id, RequestType = RequestType.TvShow, diff --git a/src/Ombi.Core/Engine/TvSearchEngine.cs b/src/Ombi.Core/Engine/TvSearchEngine.cs index 38ba8be1a..433cacee0 100644 --- a/src/Ombi.Core/Engine/TvSearchEngine.cs +++ b/src/Ombi.Core/Engine/TvSearchEngine.cs @@ -59,7 +59,12 @@ namespace Ombi.Core.Engine { continue; } - retVal.Add(await ProcessResult(tvMazeSearch, false)); + var mappedResult = await ProcessResult(tvMazeSearch, false); + if (mappedResult == null) + { + continue; + } + retVal.Add(mappedResult); } return retVal; } @@ -194,7 +199,7 @@ namespace Ombi.Core.Engine foreach (var tvMazeSearch in items) { var result = await ProcessResult(tvMazeSearch, includeImages); - if(settings.HideAvailableFromDiscover && result.Available) + if (result == null || settings.HideAvailableFromDiscover && result.Available) { continue; } @@ -211,15 +216,17 @@ namespace Ombi.Core.Engine private async Task ProcessResult(SearchTvShowViewModel item, bool includeImages) { + if (item.Id == 0) + { + return null; + } item.TheTvDbId = item.Id.ToString(); if (includeImages) { - - if (item.TheTvDbId.HasValue()) - { - item.BackdropPath = await _imageService.GetTvBackground(item.TheTvDbId); - } - + if (item.TheTvDbId.HasValue()) + { + item.BackdropPath = await _imageService.GetTvBackground(item.TheTvDbId); + } } await RunSearchRules(item); diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index b0a78fbb3..dc009371a 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -249,6 +249,26 @@ namespace Ombi.Core.Engine.V2 return result; } + public async Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken) + { + var providers = await MovieApi.GetMovieWatchProviders(movieDbId, cancellationToken); + var results = await GetUserWatchProvider(providers); + + var data = new List(); + + foreach (var result in results) + { + data.Add(new StreamingData + { + Logo = result.logo_path, + Order = result.display_priority, + StreamingProvider = result.provider_name + }); + } + + return data; + } + protected async Task> TransformMovieResultsToResponse( IEnumerable movies) { @@ -287,6 +307,7 @@ namespace Ombi.Core.Engine.V2 mapped.Requested = viewMovie.Requested; mapped.PlexUrl = viewMovie.PlexUrl; mapped.EmbyUrl = viewMovie.EmbyUrl; + mapped.JellyfinUrl = viewMovie.JellyfinUrl; mapped.Subscribed = viewMovie.Subscribed; mapped.ShowSubscribe = viewMovie.ShowSubscribe; diff --git a/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs index 13256be0a..b7d9575db 100644 --- a/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using Ombi.Api.Lidarr; using Ombi.Api.Lidarr.Models; using Ombi.Api.MusicBrainz; @@ -41,6 +42,21 @@ namespace Ombi.Core.Engine.V2 _lidarrApi = lidarrApi; } + public async Task GetAlbum(string albumId) + { + var g = await _musicBrainzApi.GetAlbumInformation(albumId); + var release = new ReleaseGroup + { + ReleaseType = g.ReleaseGroup.PrimaryType, + Id = g.Id, + Title = g.Title, + ReleaseDate = g.ReleaseGroup.FirstReleaseDate, + }; + + await RunSearchRules(release); + return release; + } + public async Task GetArtistInformation(string artistId) { var artist = await _musicBrainzApi.GetArtistInformation(artistId); @@ -84,12 +100,19 @@ namespace Ombi.Core.Engine.V2 if (lidarrArtistTask != null) { - var artistResult = await lidarrArtistTask; - info.Banner = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("banner", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.Logo = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("logo", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.Poster = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("poster", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.FanArt = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("fanart", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.Overview = artistResult.overview; + try + { + var artistResult = await lidarrArtistTask; + info.Banner = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("banner", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.Logo = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("logo", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.Poster = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("poster", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.FanArt = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("fanart", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.Overview = artistResult.overview; + } + catch (JsonSerializationException) + { + // swallow, Lidarr probably doesn't have this artist + } } return info; @@ -118,7 +141,7 @@ namespace Ombi.Core.Engine.V2 return new AlbumArt(); } - + public async Task GetArtistInformationByRequestId(int requestId) { var request = await RequestService.MusicRequestRepository.Find(requestId); diff --git a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs index dd2ce22aa..29ea01879 100644 --- a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs @@ -19,6 +19,8 @@ using Ombi.Core.Settings; using Ombi.Store.Repository; using TraktSharp.Entities; using Microsoft.EntityFrameworkCore; +using System.Threading; +using Ombi.Api.TheMovieDb; namespace Ombi.Core.Engine.V2 { @@ -27,15 +29,17 @@ namespace Ombi.Core.Engine.V2 private readonly ITvMazeApi _tvMaze; private readonly IMapper _mapper; private readonly ITraktApi _traktApi; + private readonly IMovieDbApi _movieApi; public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService s, - IRepository sub) + IRepository sub, IMovieDbApi movieApi) : base(identity, service, r, um, memCache, s, sub) { _tvMaze = tvMaze; _mapper = mapper; _traktApi = trakt; + _movieApi = movieApi; } @@ -106,6 +110,39 @@ namespace Ombi.Core.Engine.V2 return await ProcessResult(mapped, traktInfoTask); } + public async Task> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken) + { + var tvdbshow = await Cache.GetOrAdd(nameof(GetShowInformation) + tvMazeId, + async () => await _tvMaze.ShowLookupByTheTvDbId(tvMazeId), DateTime.Now.AddHours(12)); + if (tvdbshow == null) + { + return null; + } + + /// this is a best effort guess since TV maze do not provide the TheMovieDbId + var movieDbResults = await _movieApi.SearchTv(tvdbshow.name, tvdbshow.premiered.Substring(0, 4)); + var potential = movieDbResults.FirstOrDefault(); + tvDbId = potential.Id; + // end guess + + var providers = await _movieApi.GetTvWatchProviders(tvDbId, cancellationToken); + var results = await GetUserWatchProvider(providers); + + var data = new List(); + + foreach (var result in results) + { + data.Add(new StreamingData + { + Logo = result.logo_path, + Order = result.display_priority, + StreamingProvider = result.provider_name + }); + } + + return data; + } + private IEnumerable ProcessResults(IEnumerable items) { var retVal = new List(); @@ -141,7 +178,7 @@ namespace Ombi.Core.Engine.V2 { item.Images.Medium = item.Images.Medium.ToHttpsUrl(); } - + if (item.Cast?.Any() ?? false) { foreach (var cast in item.Cast) diff --git a/src/Ombi.Core/Helpers/WatchProviderParser.cs b/src/Ombi.Core/Helpers/WatchProviderParser.cs new file mode 100644 index 000000000..68f3c26dd --- /dev/null +++ b/src/Ombi.Core/Helpers/WatchProviderParser.cs @@ -0,0 +1,35 @@ +using Ombi.Api.TheMovieDb.Models; +using Ombi.Store.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ombi.Core.Helpers +{ + public static class WatchProviderParser + { + public static List GetUserWatchProviders(WatchProviders providers, OmbiUser user) + { + var data = new List(); + + if (providers?.Results == null) + { + return data; + } + + var resultsProp = providers.Results.GetType().GetProperties(); + var matchingStreamingCountry = resultsProp.FirstOrDefault(x => x.Name.Equals(user.StreamingCountry, StringComparison.InvariantCultureIgnoreCase)); + if (matchingStreamingCountry == null) + { + return data; + } + + var result = (WatchProviderData)matchingStreamingCountry.GetValue(providers.Results); + if (result == null || result.StreamInformation == null) + { + return data; + } + return result.StreamInformation; + } + } +} diff --git a/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs index c63ea98d3..4ed713453 100644 --- a/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs +++ b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs @@ -18,6 +18,7 @@ namespace Ombi.Core.Models public enum RecentlyAddedType { Plex, - Emby + Emby, + Jellyfin } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs b/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs index 5a79d2982..22d1cc449 100644 --- a/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs +++ b/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs @@ -33,6 +33,7 @@ namespace Ombi.Core.Models.Requests { public int TheMovieDbId { get; set; } public string LanguageCode { get; set; } = "en"; + public string RequestOnBehalf { get; set; } /// /// This is only set from a HTTP Header diff --git a/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs b/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs index c17925b1b..15349462b 100644 --- a/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs +++ b/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs @@ -12,6 +12,8 @@ namespace Ombi.Core.Models.Requests public List Seasons { get; set; } = new List(); [JsonIgnore] public string RequestedByAlias { get; set; } + + public string RequestOnBehalf { get; set; } } public class SeasonsViewModel diff --git a/src/Ombi.Core/Models/Search/SearchViewModel.cs b/src/Ombi.Core/Models/Search/SearchViewModel.cs index 2f951c97e..4cf812982 100644 --- a/src/Ombi.Core/Models/Search/SearchViewModel.cs +++ b/src/Ombi.Core/Models/Search/SearchViewModel.cs @@ -14,11 +14,12 @@ namespace Ombi.Core.Models.Search public bool Available { get; set; } public string PlexUrl { get; set; } public string EmbyUrl { get; set; } + public string JellyfinUrl { get; set; } public string Quality { get; set; } public abstract RequestType Type { get; } /// - /// This is used for the PlexAvailabilityCheck/EmbyAvailabilityRule rule + /// This is used for the PlexAvailabilityCheck/EmbyAvailabilityRule/JellyfinAvailabilityRule rule /// /// /// The custom identifier. @@ -35,4 +36,4 @@ namespace Ombi.Core.Models.Search [NotMapped] public bool ShowSubscribe { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Models/Search/V2/StreamingData.cs b/src/Ombi.Core/Models/Search/V2/StreamingData.cs new file mode 100644 index 000000000..d9444c2ce --- /dev/null +++ b/src/Ombi.Core/Models/Search/V2/StreamingData.cs @@ -0,0 +1,9 @@ +namespace Ombi.Core.Models.Search.V2 +{ + public class StreamingData + { + public int Order { get; set; } + public string StreamingProvider { get; set; } + public string Logo { get; set; } + } +} diff --git a/src/Ombi.Core/Models/TesterResultModel.cs b/src/Ombi.Core/Models/TesterResultModel.cs new file mode 100644 index 000000000..f23e14d43 --- /dev/null +++ b/src/Ombi.Core/Models/TesterResultModel.cs @@ -0,0 +1,8 @@ +namespace Ombi.Core.Models +{ + public class TesterResultModel + { + public bool IsValid { get; set; } + public string ExpectedSubDir { get; set; } + } +} diff --git a/src/Ombi.Core/Models/UI/UserViewModel.cs b/src/Ombi.Core/Models/UI/UserViewModel.cs index e74e5037d..0c9be846a 100644 --- a/src/Ombi.Core/Models/UI/UserViewModel.cs +++ b/src/Ombi.Core/Models/UI/UserViewModel.cs @@ -18,6 +18,7 @@ namespace Ombi.Core.Models.UI public UserType UserType { get; set; } public int MovieRequestLimit { get; set; } public int EpisodeRequestLimit { get; set; } + public string StreamingCountry { get; set; } public RequestQuotaCountModel EpisodeRequestQuota { get; set; } public RequestQuotaCountModel MovieRequestQuota { get; set; } public RequestQuotaCountModel MusicRequestQuota { get; set; } @@ -30,4 +31,10 @@ namespace Ombi.Core.Models.UI public string Value { get; set; } public bool Enabled { get; set; } } + + public class UserViewModelDropdown + { + public string Id { get; set; } + public string Username { get; set; } + } } \ No newline at end of file diff --git a/src/Ombi.Core/Models/UserDto.cs b/src/Ombi.Core/Models/UserDto.cs index 5c629fb39..7fbdb3465 100644 --- a/src/Ombi.Core/Models/UserDto.cs +++ b/src/Ombi.Core/Models/UserDto.cs @@ -19,6 +19,7 @@ namespace Ombi.Core.Models { LocalUser = 1, PlexUser = 2, - EmbyUser = 3 + EmbyUser = 3, + JellyfinUser = 5 } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Ombi.Core.csproj b/src/Ombi.Core/Ombi.Core.csproj index 8acbaceb0..8fa0f976f 100644 --- a/src/Ombi.Core/Ombi.Core.csproj +++ b/src/Ombi.Core/Ombi.Core.csproj @@ -24,6 +24,7 @@ + @@ -40,4 +41,4 @@ - \ No newline at end of file + diff --git a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs index 24faa3a97..9076cd232 100644 --- a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs +++ b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs @@ -108,10 +108,40 @@ namespace Ombi.Core.Rule.Rules.Search x.Series.TvDbId == item.TvDbId); } + if (epExists != null) + { + episode.Available = true; + } + } + public static async Task SingleEpisodeCheck(bool useImdb, IQueryable allEpisodes, EpisodeRequests episode, + SeasonRequests season, JellyfinContent item, bool useTheMovieDb, bool useTvDb) + { + JellyfinEpisode epExists = null; + if (useImdb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + 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); + } + + if (useTvDb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.TvDbId == item.TvDbId); + } + if (epExists != null) { episode.Available = true; } } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs index 75b6633bb..3fe11cbc4 100644 --- a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs @@ -70,11 +70,11 @@ namespace Ombi.Core.Rule.Rules.Search var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); if ((server?.ServerHostname ?? string.Empty).HasValue()) { - obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname, s.IsJellyfin); + obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname); } else { - obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, null, s.IsJellyfin); + obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, null); } } @@ -100,4 +100,4 @@ namespace Ombi.Core.Rule.Rules.Search return Success(); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs new file mode 100644 index 000000000..0447458d9 --- /dev/null +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs @@ -0,0 +1,104 @@ +using System.Linq; +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; + +namespace Ombi.Core.Rule.Rules.Search +{ + public class JellyfinAvailabilityRule : BaseSearchRule, IRules + { + public JellyfinAvailabilityRule(IJellyfinContentRepository repo, ISettingsService s) + { + JellyfinContentRepository = repo; + JellyfinSettings = s; + } + + private IJellyfinContentRepository JellyfinContentRepository { get; } + private ISettingsService JellyfinSettings { get; } + + public async Task Execute(SearchViewModel obj) + { + JellyfinContent item = null; + var useImdb = false; + var useTheMovieDb = false; + var useTvDb = false; + + if (obj.ImdbId.HasValue()) + { + item = await JellyfinContentRepository.GetByImdbId(obj.ImdbId); + if (item != null) + { + useImdb = true; + } + } + if (item == null) + { + if (obj.TheMovieDbId.HasValue()) + { + item = await JellyfinContentRepository.GetByTheMovieDbId(obj.TheMovieDbId); + if (item != null) + { + useTheMovieDb = true; + } + } + + if (item == null) + { + if (obj.TheTvDbId.HasValue()) + { + item = await JellyfinContentRepository.GetByTvDbId(obj.TheTvDbId); + if (item != null) + { + useTvDb = true; + } + } + } + } + + if (item != null) + { + obj.Available = true; + var s = await JellyfinSettings.GetSettingsAsync(); + if (s.Enable) + { + var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); + if ((server?.ServerHostname ?? string.Empty).HasValue()) + { + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, server?.ServerHostname); + } + else + { + var firstServer = s.Servers?.FirstOrDefault(); + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, firstServer.ServerId, firstServer.FullUri); + } + } + + if (obj.Type == RequestType.TvShow) + { + var search = (SearchTvShowViewModel)obj; + // Let's go through the episodes now + if (search.SeasonRequests.Any()) + { + var allEpisodes = JellyfinContentRepository.GetAllEpisodes().Include(x => x.Series); + foreach (var season in search.SeasonRequests) + { + foreach (var episode in season.Episodes) + { + await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb); + } + } + } + + AvailabilityRuleHelper.CheckForUnairedEpisodes(search); + } + } + return Success(); + } + } +} diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 76f4879ce..d5e577d84 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Ombi.Api.Discord; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.Plex; using Ombi.Api.Radarr; using Ombi.Api.Sonarr; @@ -47,6 +48,7 @@ using Ombi.Core.Senders; using Ombi.Helpers; using Ombi.Schedule.Jobs.Couchpotato; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Sonarr; @@ -65,6 +67,7 @@ using Quartz.Spi; using Ombi.Api.MusicBrainz; using Ombi.Api.Twilio; using Ombi.Api.CloudService; +using Ombi.Api.RottenTomatoes; namespace Ombi.DependencyInjection { @@ -126,6 +129,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -153,8 +157,9 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) { @@ -169,6 +174,7 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -213,6 +219,9 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -220,6 +229,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index bbfe532eb..ed1e9e4a2 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs b/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs index a68ce327b..cb41ab31d 100644 --- a/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Ombi.Api.CouchPotato; using Ombi.Api.Emby; using Ombi.Api.Emby.Models; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; using Ombi.Api.Plex; using Ombi.Api.Plex.Models.Status; using Ombi.Core.Settings; diff --git a/src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs b/src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs new file mode 100644 index 000000000..2f758961c --- /dev/null +++ b/src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; +using Ombi.Api.Plex; +using Ombi.Api.Plex.Models.Status; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Ombi.HealthChecks.Checks +{ + public class JellyfinHealthCheck : BaseHealthCheck + { + public JellyfinHealthCheck(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + } + public override async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + using (var scope = CreateScope()) + { + var settingsProvider = scope.ServiceProvider.GetRequiredService>(); + var api = scope.ServiceProvider.GetRequiredService(); + var settings = await settingsProvider.GetSettingsAsync(); + if (settings == null) + { + return HealthCheckResult.Healthy("Jellyfin is not configured."); + } + + var client = api.CreateClient(settings); + var taskResult = new List>(); + foreach (var server in settings.Servers) + { + taskResult.Add(client.GetSystemInformation(server.ApiKey, server.FullUri)); + } + + try + { + var result = await Task.WhenAll(taskResult.ToArray()); + return HealthCheckResult.Healthy(); + } + catch (Exception e) + { + return HealthCheckResult.Unhealthy("Could not communicate with Jellyfin", e); + } + } + } + } +} diff --git a/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs b/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs index c348e8edf..68fb5fa84 100644 --- a/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Ombi.Api.CouchPotato; using Ombi.Api.Emby; using Ombi.Api.Emby.Models; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; using Ombi.Api.Plex; using Ombi.Api.Plex.Models.Status; using Ombi.Api.SickRage; diff --git a/src/Ombi.HealthChecks/HealthCheckExtensions.cs b/src/Ombi.HealthChecks/HealthCheckExtensions.cs index e608d5ec0..2f80378ff 100644 --- a/src/Ombi.HealthChecks/HealthCheckExtensions.cs +++ b/src/Ombi.HealthChecks/HealthCheckExtensions.cs @@ -12,6 +12,7 @@ namespace Ombi.HealthChecks { builder.AddCheck("Plex", tags: new string[] { "MediaServer" }); builder.AddCheck("Emby", tags: new string[] { "MediaServer" }); + builder.AddCheck("Jellyfin", tags: new string[] { "MediaServer" }); builder.AddCheck("Lidarr", tags: new string[] { "DVR" }); builder.AddCheck("Sonarr", tags: new string[] { "DVR" }); builder.AddCheck("Radarr", tags: new string[] { "DVR" }); diff --git a/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj b/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj index 5171c9c36..20cb07609 100644 --- a/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj +++ b/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Ombi.Helpers.Tests/EmbyHelperTests.cs b/src/Ombi.Helpers.Tests/EmbyHelperTests.cs index 261ba87cc..50ac9c48f 100644 --- a/src/Ombi.Helpers.Tests/EmbyHelperTests.cs +++ b/src/Ombi.Helpers.Tests/EmbyHelperTests.cs @@ -15,13 +15,6 @@ namespace Ombi.Helpers.Tests return EmbyHelper.GetEmbyMediaUrl(mediaId, serverId, url); } - [TestCaseSource(nameof(JellyfinUrlData))] - public string TestJellyfinUrl(string mediaId, string url, string serverId) - { - // http://192.168.68.X:8097/web/index.html#!/details?id=7ffe222498445d5ebfddb31bc4fa9a6d&serverId=50cce67f0baa425093d189b3017331fb - return EmbyHelper.GetEmbyMediaUrl(mediaId, serverId, url, true); - } - public static IEnumerable UrlData { get @@ -33,16 +26,5 @@ namespace Ombi.Helpers.Tests yield return new TestCaseData(mediaId.ToString(), string.Empty, "1").Returns($"https://app.emby.media/web/index.html#!/item?id={mediaId}&serverId=1").SetName("EmbyHelper_GetMediaUrl_WithOutCustomDomain"); } } - - public static IEnumerable JellyfinUrlData - { - get - { - var mediaId = 1; - yield return new TestCaseData(mediaId.ToString(), "http://google.com", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash"); - yield return new TestCaseData(mediaId.ToString(), "http://google.com/", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain"); - yield return new TestCaseData(mediaId.ToString(), "https://google.com/", "1").Returns($"https://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain_Https"); - } - } } } diff --git a/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs b/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs new file mode 100644 index 000000000..df3f960d4 --- /dev/null +++ b/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Helpers.Tests +{ + [TestFixture] + public class JellyfinHelperTests + { + [TestCaseSource(nameof(UrlData))] + public string TestUrl(string mediaId, string url, string serverId) + { + // http://192.168.68.X:8097/web/index.html#!/details?id=7ffe222498445d5ebfddb31bc4fa9a6d&serverId=50cce67f0baa425093d189b3017331fb + return JellyfinHelper.GetJellyfinMediaUrl(mediaId, serverId, url); + } + + public static IEnumerable UrlData + { + get + { + var mediaId = 1; + yield return new TestCaseData(mediaId.ToString(), "http://google.com", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash"); + yield return new TestCaseData(mediaId.ToString(), "http://google.com/", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain"); + yield return new TestCaseData(mediaId.ToString(), "https://google.com/", "1").Returns($"https://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain_Https"); + } + } + } +} diff --git a/src/Ombi.Helpers/EmbyHelper.cs b/src/Ombi.Helpers/EmbyHelper.cs index 785ca47d4..db739b375 100644 --- a/src/Ombi.Helpers/EmbyHelper.cs +++ b/src/Ombi.Helpers/EmbyHelper.cs @@ -2,14 +2,10 @@ { public static class EmbyHelper { - public static string GetEmbyMediaUrl(string mediaId, string serverId, string customerServerUrl = null, bool isJellyfin = false) + public static string GetEmbyMediaUrl(string mediaId, string serverId, string customerServerUrl = null) { //web/index.html#!/details|item string path = "item"; - if (isJellyfin) - { - path = "details"; - } if (customerServerUrl.HasValue()) { if (!customerServerUrl.EndsWith("/")) diff --git a/src/Ombi.Helpers/JellyfinHelper.cs b/src/Ombi.Helpers/JellyfinHelper.cs new file mode 100644 index 000000000..506341d7d --- /dev/null +++ b/src/Ombi.Helpers/JellyfinHelper.cs @@ -0,0 +1,23 @@ +namespace Ombi.Helpers +{ + public static class JellyfinHelper + { + public static string GetJellyfinMediaUrl(string mediaId, string serverId, string customerServerUrl = null) + { + //web/index.html#!/details|item + string path = "details"; + if (customerServerUrl.HasValue()) + { + if (!customerServerUrl.EndsWith("/")) + { + return $"{customerServerUrl}/web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + } + return $"{customerServerUrl}web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + } + else + { + return $"http://localhost:8096/web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + } + } + } +} diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index a7c61d9d2..ed6a7bc2a 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -14,8 +14,10 @@ namespace Ombi.Helpers public static EventId RadarrCacher => new EventId(2001); public static EventId PlexEpisodeCacher => new EventId(2002); public static EventId EmbyContentCacher => new EventId(2003); + public static EventId JellyfinContentCacher => new EventId(2012); public static EventId PlexUserImporter => new EventId(2004); public static EventId EmbyUserImporter => new EventId(2005); + public static EventId JellyfinUserImporter => new EventId(2013); public static EventId SonarrCacher => new EventId(2006); public static EventId CouchPotatoCacher => new EventId(2007); public static EventId PlexContentCacher => new EventId(2008); @@ -43,4 +45,4 @@ namespace Ombi.Helpers public static EventId Updater => new EventId(6000); } -} \ No newline at end of file +} diff --git a/src/Ombi.Helpers/PlexHelper.cs b/src/Ombi.Helpers/PlexHelper.cs index 4dbde0c92..08548b8c9 100644 --- a/src/Ombi.Helpers/PlexHelper.cs +++ b/src/Ombi.Helpers/PlexHelper.cs @@ -42,7 +42,7 @@ namespace Ombi.Helpers //com.plexapp.agents.themoviedb://390043?lang=en //com.plexapp.agents.imdb://tt2543164?lang=en //plex://movie/5e1632df2d4d84003e48e54e - // https://github.com/tidusjar/Ombi/issues/3277 + // https://github.com/Ombi-app/Ombi/issues/3277 if (string.IsNullOrEmpty(guid)) { return new ProviderId(); diff --git a/src/Ombi.Helpers/StartupSingleton.cs b/src/Ombi.Helpers/StartupSingleton.cs index 41fd1a9ab..c9df7665a 100644 --- a/src/Ombi.Helpers/StartupSingleton.cs +++ b/src/Ombi.Helpers/StartupSingleton.cs @@ -11,5 +11,8 @@ public string StoragePath { get; set; } public string SecurityKey { get; set; } +#if DEBUG + = "test"; +#endif } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/TvProfile.cs b/src/Ombi.Mapping/Profiles/TvProfile.cs index 8cb4e38b6..0e1378d95 100644 --- a/src/Ombi.Mapping/Profiles/TvProfile.cs +++ b/src/Ombi.Mapping/Profiles/TvProfile.cs @@ -47,7 +47,7 @@ namespace Ombi.Mapping.Profiles CreateMap() - .ForMember(dest => dest.Id, opts => opts.MapFrom(src => Convert.ToInt32(src.Ids.Tvdb.ToString()))) + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Ids.Tvdb.HasValue ? Convert.ToInt32(src.Ids.Tvdb.ToString()) : 0)) .ForMember(dest => dest.FirstAired, opts => opts.MapFrom(src => src.FirstAired.HasValue ? src.FirstAired.Value.ToString("yyyy-MM-ddTHH:mm:ss") : string.Empty)) .ForMember(dest => dest.ImdbId, opts => opts.MapFrom(src => src.Ids.Imdb)) .ForMember(dest => dest.Network, opts => opts.MapFrom(src => src.Network)) @@ -57,9 +57,9 @@ namespace Ombi.Mapping.Profiles .ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title)) .ForMember(dest => dest.Status, opts => opts.MapFrom(src => TraktEnumHelper.GetDescription(src.Status))) .ForMember(dest => dest.Trailer, - opts => opts.MapFrom(src => src.Trailer.ToString().ToHttpsUrl())) + opts => opts.MapFrom(src => src.Trailer != null ? src.Trailer.ToString().ToHttpsUrl() : string.Empty)) .ForMember(dest => dest.Homepage, - opts => opts.MapFrom(src => src.Homepage.ToString().ToHttpsUrl())); + opts => opts.MapFrom(src => src.Homepage != null ? src.Homepage.ToString().ToHttpsUrl() : string.Empty)); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html b/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html index ac4c4a119..9f2564b99 100644 --- a/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html +++ b/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html @@ -166,7 +166,7 @@
- Powered by Ombi {@DATENOW} + Powered by Ombi {@DATENOW}
diff --git a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html index 5456743c9..10b76dd30 100644 --- a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html +++ b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html @@ -1,4 +1,4 @@ - + @@ -453,7 +453,7 @@ - Powered by Ombi + Powered by Ombi diff --git a/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs b/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs index 98a0c0910..cc05b711a 100644 --- a/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs +++ b/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs @@ -77,12 +77,16 @@ namespace Ombi.Schedule.Jobs.Couchpotato var movies = await _api.GetMovies(settings.FullUri, settings.ApiKey, new[] {"active"}); if (movies != null) { - // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); - tran.Commit(); - } + // Let's remove the old cached data + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); + tran.Commit(); + } + }); // Save var movieIds = new List(); @@ -102,14 +106,17 @@ namespace Ombi.Schedule.Jobs.Couchpotato _log.LogError("TMDBId is not > 0 for movie {0}", m.title); } } - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.CouchPotatoCache.AddRangeAsync(movieIds); + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.CouchPotatoCache.AddRangeAsync(movieIds); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Finished"); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index f8bde2755..866216fe4 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -58,7 +58,7 @@ namespace Ombi.Schedule.Jobs.Emby { await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Emby Content Sync Failed"); - _logger.LogError(e, "Exception when caching {1} for server {0}", server.Name, embySettings.IsJellyfin ? "Jellyfin" : "Emby"); + _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); } } @@ -145,7 +145,7 @@ namespace Ombi.Schedule.Jobs.Emby Title = tvShow.Name, Type = EmbyMediaType.Series, EmbyId = tvShow.Id, - Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname, settings.IsJellyfin), + Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname), AddedAt = DateTime.UtcNow }); } @@ -228,4 +228,4 @@ namespace Ombi.Schedule.Jobs.Emby } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs index 1207e1f42..4b684b8ab 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs @@ -80,7 +80,7 @@ namespace Ombi.Schedule.Jobs.Emby Api = _apiFactory.CreateClient(settings); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) - .SendAsync(NotificationHub.NotificationEvent, $"{(settings.IsJellyfin ? "Jellyfin" : "Emby")} User Importer Started"); + .SendAsync(NotificationHub.NotificationEvent, $"Emby User Importer Started"); var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); foreach (var server in settings.Servers) { @@ -117,7 +117,8 @@ namespace Ombi.Schedule.Jobs.Emby ProviderUserId = embyUser.Id, Alias = isConnectUser ? embyUser.Name : string.Empty, MovieRequestLimit = userManagementSettings.MovieRequestLimit, - EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit + EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit, + StreamingCountry = userManagementSettings.DefaultStreamingCountry }; var result = await _userManager.CreateAsync(newUser); if (!result.Succeeded) @@ -180,4 +181,4 @@ namespace Ombi.Schedule.Jobs.Emby GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs new file mode 100644 index 000000000..7b89fa3f2 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinAvaliabilityChecker : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs new file mode 100644 index 000000000..e9ef28cc6 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinContentSync : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs new file mode 100644 index 000000000..53392cc6a --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinEpisodeSync : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs new file mode 100644 index 000000000..06ff204f8 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinUserImporter : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs new file mode 100644 index 000000000..60d017d93 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs @@ -0,0 +1,235 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinAvaliabilityCheker.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ombi.Core; +using Ombi.Core.Notifications; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Notifications.Models; +using Ombi.Schedule.Jobs.Ombi; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; +using Quartz; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinAvaliabilityChecker : IJellyfinAvaliabilityChecker + { + public JellyfinAvaliabilityChecker(IJellyfinContentRepository repo, ITvRequestRepository t, IMovieRequestRepository m, + INotificationHelper n, ILogger log, IHubContext notification) + { + _repo = repo; + _tvRepo = t; + _movieRepo = m; + _notificationService = n; + _log = log; + _notification = notification; + } + + private readonly ITvRequestRepository _tvRepo; + private readonly IMovieRequestRepository _movieRepo; + private readonly IJellyfinContentRepository _repo; + private readonly INotificationHelper _notificationService; + private readonly ILogger _log; + private readonly IHubContext _notification; + + public async Task Execute(IJobExecutionContext job) + { + _log.LogInformation("Starting Jellyfin Availability Check"); + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Started"); + await ProcessMovies(); + await ProcessTv(); + + _log.LogInformation("Finished Jellyfin Availability Check"); + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Finished"); + } + + private async Task ProcessMovies() + { + var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available); + + foreach (var movie in movies) + { + JellyfinContent jellyfinContent = null; + if (movie.TheMovieDbId > 0) + { + jellyfinContent = await _repo.GetByTheMovieDbId(movie.TheMovieDbId.ToString()); + } + else if(movie.ImdbId.HasValue()) + { + jellyfinContent = await _repo.GetByImdbId(movie.ImdbId); + } + + if (jellyfinContent == null) + { + // We don't have this yet + continue; + } + + _log.LogInformation("We have found the request {0} on Jellyfin, sending the notification", movie?.Title ?? string.Empty); + + movie.Available = true; + movie.MarkedAsAvailable = DateTime.Now; + if (movie.Available) + { + var recipient = movie.RequestedUser.Email.HasValue() ? movie.RequestedUser.Email : string.Empty; + + _log.LogDebug("MovieId: {0}, RequestUser: {1}", movie.Id, recipient); + + await _notificationService.Notify(new NotificationOptions + { + DateTime = DateTime.Now, + NotificationType = NotificationType.RequestAvailable, + RequestId = movie.Id, + RequestType = RequestType.Movie, + Recipient = recipient, + }); + } + } + await _movieRepo.Save(); + } + + + + /// + /// TODO This is EXCATLY the same as the PlexAvailabilityChecker. Refactor Please future Jamie + /// + /// + private async Task ProcessTv() + { + var tv = _tvRepo.GetChild().Where(x => !x.Available); + var jellyfinEpisodes = _repo.GetAllEpisodes().Include(x => x.Series); + + foreach (var child in tv) + { + + var useImdb = false; + var useTvDb = false; + if (child.ParentRequest.ImdbId.HasValue()) + { + useImdb = true; + } + + if (child.ParentRequest.TvDbId.ToString().HasValue()) + { + useTvDb = true; + } + + var tvDbId = child.ParentRequest.TvDbId; + var imdbId = child.ParentRequest.ImdbId; + IQueryable seriesEpisodes = null; + if (useImdb) + { + seriesEpisodes = jellyfinEpisodes.Where(x => x.Series.ImdbId == imdbId.ToString()); + } + + if (useTvDb && (seriesEpisodes == null || !seriesEpisodes.Any())) + { + seriesEpisodes = jellyfinEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString()); + } + + if (seriesEpisodes == null) + { + continue; + } + + if (!seriesEpisodes.Any()) + { + // Let's try and match the series by name + seriesEpisodes = jellyfinEpisodes.Where(x => + x.Series.Title == child.Title); + } + + foreach (var season in child.SeasonRequests) + { + foreach (var episode in season.Episodes) + { + if (episode.Available) + { + continue; + } + + var foundEp = await seriesEpisodes.FirstOrDefaultAsync( + x => x.EpisodeNumber == episode.EpisodeNumber && + x.SeasonNumber == episode.Season.SeasonNumber); + + if (foundEp != null) + { + episode.Available = true; + } + } + } + + // Check to see if all of the episodes in all seasons are available for this request + var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available)); + if (allAvailable) + { + // We have fulfulled this request! + child.Available = true; + child.MarkedAsAvailable = DateTime.Now; + await _notificationService.Notify(new NotificationOptions + { + DateTime = DateTime.Now, + NotificationType = NotificationType.RequestAvailable, + RequestId = child.Id, + RequestType = RequestType.TvShow, + Recipient = child.RequestedUser.Email + }); + } + } + + await _tvRepo.Save(); + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs new file mode 100644 index 000000000..ff96e2130 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models.Movie; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Schedule.Jobs.Ombi; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Quartz; +using JellyfinMediaType = Ombi.Store.Entities.JellyfinMediaType; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinContentSync : IJellyfinContentSync + { + public JellyfinContentSync(ISettingsService settings, IJellyfinApiFactory api, ILogger logger, + IJellyfinContentRepository repo, IHubContext notification) + { + _logger = logger; + _settings = settings; + _apiFactory = api; + _repo = repo; + _notification = notification; + } + + private readonly ILogger _logger; + private readonly ISettingsService _settings; + private readonly IJellyfinApiFactory _apiFactory; + private readonly IJellyfinContentRepository _repo; + private readonly IHubContext _notification; + private IJellyfinApi Api { get; set; } + + public async Task Execute(IJobExecutionContext job) + { + var jellyfinSettings = await _settings.GetSettingsAsync(); + if (!jellyfinSettings.Enable) + return; + + Api = _apiFactory.CreateClient(jellyfinSettings); + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Started"); + + foreach (var server in jellyfinSettings.Servers) + { + try + { + await StartServerCache(server, jellyfinSettings); + } + catch (Exception e) + { + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Failed"); + _logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name); + } + } + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished"); + // Episodes + + await OmbiQuartz.TriggerJob(nameof(IJellyfinEpisodeSync), "Jellyfin"); + } + + + private async Task StartServerCache(JellyfinServers server, JellyfinSettings settings) + { + if (!ValidateSettings(server)) + return; + + //await _repo.ExecuteSql("DELETE FROM JellyfinEpisode"); + //await _repo.ExecuteSql("DELETE FROM JellyfinContent"); + + var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var totalCount = movies.TotalRecordCount; + var processed = 1; + + var mediaToAdd = new HashSet(); + + while (processed < totalCount) + { + foreach (var movie in movies.Items) + { + if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) + { + var movieInfo = + await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); + foreach (var item in movieInfo.Items) + { + await ProcessMovies(item, mediaToAdd, server); + } + + processed++; + } + else + { + processed++; + // Regular movie + await ProcessMovies(movie, mediaToAdd, server); + } + } + + // Get the next batch + movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + + } + + + // TV Time + var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var totalTv = tv.TotalRecordCount; + processed = 1; + while (processed < totalTv) + { + foreach (var tvShow in tv.Items) + { + try + { + + processed++; + if (string.IsNullOrEmpty(tvShow.ProviderIds?.Tvdb)) + { + _logger.LogInformation("Provider Id on tv {0} is null", tvShow.Name); + continue; + } + + var existingTv = await _repo.GetByJellyfinId(tvShow.Id); + if (existingTv == null) + { + _logger.LogDebug("Adding new TV Show {0}", tvShow.Name); + mediaToAdd.Add(new JellyfinContent + { + TvDbId = tvShow.ProviderIds?.Tvdb, + ImdbId = tvShow.ProviderIds?.Imdb, + TheMovieDbId = tvShow.ProviderIds?.Tmdb, + Title = tvShow.Name, + Type = JellyfinMediaType.Series, + JellyfinId = tvShow.Id, + Url = JellyfinHelper.GetJellyfinMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname), + AddedAt = DateTime.UtcNow + }); + } + else + { + _logger.LogDebug("We already have TV Show {0}", tvShow.Name); + } + + } + catch (Exception) + { + + throw; + } + } + // Get the next batch + tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + } + + if (mediaToAdd.Any()) + await _repo.AddRange(mediaToAdd); + } + + private async Task ProcessMovies(JellyfinMovie movieInfo, ICollection content, JellyfinServers server) + { + // Check if it exists + var existingMovie = await _repo.GetByJellyfinId(movieInfo.Id); + var alreadyGoingToAdd = content.Any(x => x.JellyfinId == movieInfo.Id); + if (existingMovie == null && !alreadyGoingToAdd) + { + _logger.LogDebug("Adding new movie {0}", movieInfo.Name); + content.Add(new JellyfinContent + { + ImdbId = movieInfo.ProviderIds.Imdb, + TheMovieDbId = movieInfo.ProviderIds?.Tmdb, + Title = movieInfo.Name, + Type = JellyfinMediaType.Movie, + JellyfinId = movieInfo.Id, + Url = JellyfinHelper.GetJellyfinMediaUrl(movieInfo.Id, server?.ServerId, server.ServerHostname), + AddedAt = DateTime.UtcNow, + }); + } + else + { + // we have this + _logger.LogDebug("We already have movie {0}", movieInfo.Name); + } + } + + private bool ValidateSettings(JellyfinServers server) + { + if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) + { + _logger.LogInformation(LoggingEvents.JellyfinContentCacher, $"Server {server?.Name} is not configured correctly"); + return false; + } + + return true; + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + //_settings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs new file mode 100644 index 000000000..11c7ec7af --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs @@ -0,0 +1,181 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinEpisodeCacher.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Ombi.Api.Jellyfin; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Hubs; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Quartz; +using Ombi.Schedule.Jobs.Ombi; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinEpisodeSync : IJellyfinEpisodeSync + { + public JellyfinEpisodeSync(ISettingsService s, IJellyfinApiFactory api, ILogger l, IJellyfinContentRepository repo + , IHubContext notification) + { + _apiFactory = api; + _logger = l; + _settings = s; + _repo = repo; + _notification = notification; + } + + private readonly ISettingsService _settings; + private readonly IJellyfinApiFactory _apiFactory; + private readonly ILogger _logger; + private readonly IJellyfinContentRepository _repo; + private readonly IHubContext _notification; + private IJellyfinApi Api { get; set; } + + + public async Task Execute(IJobExecutionContext job) + { + var settings = await _settings.GetSettingsAsync(); + + Api = _apiFactory.CreateClient(settings); + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started"); + foreach (var server in settings.Servers) + { + await CacheEpisodes(server); + } + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Finished"); + _logger.LogInformation("Jellyfin Episode Sync Finished - Triggering Metadata refresh"); + await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); + } + + private async Task CacheEpisodes(JellyfinServers server) + { + var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var total = allEpisodes.TotalRecordCount; + var processed = 1; + var epToAdd = new HashSet(); + while (processed < total) + { + foreach (var ep in allEpisodes.Items) + { + processed++; + + if (ep.LocationType?.Equals("Virtual", StringComparison.InvariantCultureIgnoreCase) ?? false) + { + // For some reason Jellyfin is not respecting the `IsVirtualItem` field. + continue; + } + + // Let's make sure we have the parent request, stop those pesky forign key errors, + // Damn me having data integrity + var parent = await _repo.GetByJellyfinId(ep.SeriesId); + if (parent == null) + { + _logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this", + ep.Name); + continue; + } + + var existingEpisode = await _repo.GetEpisodeByJellyfinId(ep.Id); + // Make sure it's not in the hashset too + var existingInList = epToAdd.Any(x => x.JellyfinId == ep.Id); + + if (existingEpisode == null && !existingInList) + { + _logger.LogDebug("Adding new episode {0} to parent {1}", ep.Name, ep.SeriesName); + // add it + epToAdd.Add(new JellyfinEpisode + { + JellyfinId = ep.Id, + EpisodeNumber = ep.IndexNumber, + SeasonNumber = ep.ParentIndexNumber, + ParentId = ep.SeriesId, + TvDbId = ep.ProviderIds.Tvdb, + TheMovieDbId = ep.ProviderIds.Tmdb, + ImdbId = ep.ProviderIds.Imdb, + Title = ep.Name, + AddedAt = DateTime.UtcNow + }); + + if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) + { + epToAdd.Add(new JellyfinEpisode + { + JellyfinId = ep.Id, + EpisodeNumber = ep.IndexNumberEnd.Value, + SeasonNumber = ep.ParentIndexNumber, + ParentId = ep.SeriesId, + TvDbId = ep.ProviderIds.Tvdb, + TheMovieDbId = ep.ProviderIds.Tmdb, + ImdbId = ep.ProviderIds.Imdb, + Title = ep.Name, + AddedAt = DateTime.UtcNow + }); + } + } + } + + await _repo.AddRange(epToAdd); + epToAdd.Clear(); + allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + } + + if (epToAdd.Any()) + { + await _repo.AddRange(epToAdd); + } + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + //_settings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs new file mode 100644 index 000000000..96973580d --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs @@ -0,0 +1,174 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinUserImporter.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ombi.Api.Jellyfin; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Settings.Settings.Models; +using Ombi.Store.Entities; +using Quartz; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinUserImporter : IJellyfinUserImporter + { + public JellyfinUserImporter(IJellyfinApiFactory api, UserManager um, ILogger log, + ISettingsService jellyfinSettings, ISettingsService ums, IHubContext notification) + { + _apiFactory = api; + _userManager = um; + _log = log; + _jellyfinSettings = jellyfinSettings; + _userManagementSettings = ums; + _notification = notification; + } + + private readonly IJellyfinApiFactory _apiFactory; + private readonly UserManager _userManager; + private readonly ILogger _log; + private readonly ISettingsService _jellyfinSettings; + private readonly ISettingsService _userManagementSettings; + private readonly IHubContext _notification; + private IJellyfinApi Api { get; set; } + + public async Task Execute(IJobExecutionContext job) + { + var userManagementSettings = await _userManagementSettings.GetSettingsAsync(); + if (!userManagementSettings.ImportJellyfinUsers) + { + return; + } + var settings = await _jellyfinSettings.GetSettingsAsync(); + if (!settings.Enable) + { + return; + } + + Api = _apiFactory.CreateClient(settings); + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, $"Jellyfin User Importer Started"); + var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.JellyfinUser).ToListAsync(); + foreach (var server in settings.Servers) + { + if (string.IsNullOrEmpty(server.ApiKey)) + { + continue; + } + + var jellyfinUsers = await Api.GetUsers(server.FullUri, server.ApiKey); + foreach (var jellyfinUser in jellyfinUsers) + { + // Check if we should import this user + if (userManagementSettings.BannedJellyfinUserIds.Contains(jellyfinUser.Id)) + { + // Do not import these, they are not allowed into the country. + continue; + } + // Check if this Jellyfin User already exists + var existingJellyfinUser = allUsers.FirstOrDefault(x => x.ProviderUserId == jellyfinUser.Id); + if (existingJellyfinUser == null) + { + + if (!jellyfinUser.Name.HasValue()) + { + _log.LogInformation("Could not create Jellyfin user since the have no username, JellyfinUserId: {0}", jellyfinUser.Id); + continue; + } + // Create this users + var newUser = new OmbiUser + { + UserName = jellyfinUser.Name, + UserType = UserType.JellyfinUser, + ProviderUserId = jellyfinUser.Id, + MovieRequestLimit = userManagementSettings.MovieRequestLimit, + EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit, + StreamingCountry = userManagementSettings.DefaultStreamingCountry + }; + _log.LogInformation("Creating Jellyfin user {0}", newUser.UserName); + var result = await _userManager.CreateAsync(newUser); + if (!result.Succeeded) + { + foreach (var identityError in result.Errors) + { + _log.LogError(LoggingEvents.JellyfinUserImporter, identityError.Description); + } + continue; + } + if (userManagementSettings.DefaultRoles.Any()) + { + foreach (var defaultRole in userManagementSettings.DefaultRoles) + { + await _userManager.AddToRoleAsync(newUser, defaultRole); + } + } + } + else + { + // Do we need to update this user? + existingJellyfinUser.UserName = jellyfinUser.Name; + + await _userManager.UpdateAsync(existingJellyfinUser); + } + } + } + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin User Importer Finished"); + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _userManager?.Dispose(); + //_jellyfinSettings?.Dispose(); + //_userManagementSettings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs b/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs index 9530cf5a1..0cb1c441b 100644 --- a/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs +++ b/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs @@ -51,12 +51,16 @@ namespace Ombi.Schedule.Jobs.Lidarr var albums = await _lidarrApi.GetAllAlbums(settings.ApiKey, settings.FullUri); if (albums != null && albums.Any()) { - // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrAlbumCache"); - tran.Commit(); - } + // Let's remove the old cached data + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrAlbumCache"); + tran.Commit(); + } + }); var albumCache = new List(); foreach (var a in albums) @@ -76,14 +80,17 @@ namespace Ombi.Schedule.Jobs.Lidarr }); } } - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.LidarrAlbumCache.AddRangeAsync(albumCache); + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.LidarrAlbumCache.AddRangeAsync(albumCache); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); } } catch (System.Exception ex) diff --git a/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs b/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs index edcd6cbe6..06577beb6 100644 --- a/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs +++ b/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs @@ -50,12 +50,16 @@ namespace Ombi.Schedule.Jobs.Lidarr var artists = await _lidarrApi.GetArtists(settings.ApiKey, settings.FullUri); if (artists != null && artists.Any()) { - // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrArtistCache"); - tran.Commit(); - } + // Let's remove the old cached data + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrArtistCache"); + tran.Commit(); + } + }); var artistCache = new List(); foreach (var a in artists) @@ -71,14 +75,17 @@ namespace Ombi.Schedule.Jobs.Lidarr }); } } - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.LidarrArtistCache.AddRangeAsync(artistCache); + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.LidarrArtistCache.AddRangeAsync(artistCache); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); } } catch (Exception ex) diff --git a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index 66ce28487..d1b2d3f99 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Store.Repository; using Quartz; @@ -14,12 +16,13 @@ namespace Ombi.Schedule.Jobs.Ombi public class MediaDatabaseRefresh : IMediaDatabaseRefresh { public MediaDatabaseRefresh(ISettingsService s, ILogger log, - IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo) + IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo) { _settings = s; _log = log; _plexRepo = plexRepo; _embyRepo = embyRepo; + _jellyfinRepo = jellyfinRepo; _settings.ClearCache(); } @@ -27,6 +30,7 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly ILogger _log; private readonly IPlexContentRepository _plexRepo; private readonly IEmbyContentRepository _embyRepo; + private readonly IJellyfinContentRepository _jellyfinRepo; public async Task Execute(IJobExecutionContext job) { @@ -34,6 +38,7 @@ namespace Ombi.Schedule.Jobs.Ombi { await RemovePlexData(); await RemoveEmbyData(); + await RemoveJellyfinData(); } catch (Exception e) { @@ -64,6 +69,28 @@ namespace Ombi.Schedule.Jobs.Ombi } } + private async Task RemoveJellyfinData() + { + try + { + var s = await _settings.GetSettingsAsync(); + if (!s.Enable) + { + return; + } + const string episodeSQL = "DELETE FROM JellyfinEpisode"; + const string mainSql = "DELETE FROM JellyfinContent"; + await _jellyfinRepo.ExecuteSql(episodeSQL); + await _jellyfinRepo.ExecuteSql(mainSql); + + await OmbiQuartz.TriggerJob(nameof(IJellyfinContentSync), "Jellyfin"); + } + catch (Exception e) + { + _log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Jellyfin Data Failed"); + } + } + private async Task RemovePlexData() { try @@ -80,6 +107,9 @@ namespace Ombi.Schedule.Jobs.Ombi await _plexRepo.ExecuteSql(episodeSQL); await _plexRepo.ExecuteSql(seasonsSql); await _plexRepo.ExecuteSql(mainSql); + + + await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IPlexContentSync), "Plex"), new JobDataMap(new Dictionary { { "recentlyAddedSearch", "false" } })); } catch (Exception e) { @@ -108,4 +138,4 @@ namespace Ombi.Schedule.Jobs.Ombi GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index e9ad9333c..cbc2e6287 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -38,16 +38,17 @@ namespace Ombi.Schedule.Jobs.Ombi { public class NewsletterJob : HtmlTemplateGenerator, INewsletterJob { - public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository addedLog, + public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IJellyfinContentRepository jellyfin, IRepository addedLog, IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService custom, ISettingsService emailSettings, INotificationTemplatesRepository templateRepo, UserManager um, ISettingsService newsletter, ILogger log, ILidarrApi lidarrApi, IExternalRepository albumCache, ISettingsService lidarrSettings, - ISettingsService ombiSettings, ISettingsService plexSettings, ISettingsService embySettings - , IHubContext notification, IRefreshMetadata refreshMetadata) + ISettingsService ombiSettings, ISettingsService plexSettings, ISettingsService embySettings, ISettingsService jellyfinSettings, + IHubContext notification, IRefreshMetadata refreshMetadata) { _plex = plex; _emby = emby; + _jellyfin = jellyfin; _recentlyAddedLog = addedLog; _movieApi = movieApi; _tvApi = tvApi; @@ -64,6 +65,7 @@ namespace Ombi.Schedule.Jobs.Ombi _ombiSettings = ombiSettings; _plexSettings = plexSettings; _embySettings = embySettings; + _jellyfinSettings = jellyfinSettings; _notification = notification; _ombiSettings.ClearCache(); _plexSettings.ClearCache(); @@ -74,6 +76,7 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly IPlexContentRepository _plex; private readonly IEmbyContentRepository _emby; + private readonly IJellyfinContentRepository _jellyfin; private readonly IRepository _recentlyAddedLog; private readonly IMovieDbApi _movieApi; private readonly ITvMazeApi _tvApi; @@ -90,6 +93,7 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly ISettingsService _lidarrSettings; private readonly ISettingsService _plexSettings; private readonly ISettingsService _embySettings; + private readonly ISettingsService _jellyfinSettings; private readonly IHubContext _notification; private readonly IRefreshMetadata _refreshMetadata; @@ -123,36 +127,46 @@ namespace Ombi.Schedule.Jobs.Ombi // Get the Content var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking(); var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking(); + var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking(); var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable); var addedLog = _recentlyAddedLog.GetAll(); var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); + var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet(); var addedPlexEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode); var addedEmbyEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode); + var addedJellyfinEpisodesLogIds = + addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Episode); // Filter out the ones that we haven't sent yet var plexContentLocalDataset = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); var embyContentLocalDataset = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); + var jellyfinContentLocalDataset = jellyfinContent.Where(x => x.Type == JellyfinMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); var plexContentMoviesToSend = plexContentLocalDataset.Where(x => !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet(); var embyContentMoviesToSend = embyContentLocalDataset.Where(x => !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet(); + var jellyfinContentMoviesToSend = jellyfinContentLocalDataset.Where(x => !addedJellyfinMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet(); var lidarrContentAlbumsToSend = lidarrContent.Where(x => !addedAlbumLogIds.Contains(x.ForeignAlbumId)).ToHashSet(); _log.LogInformation("Plex Movies to send: {0}", plexContentMoviesToSend.Count()); _log.LogInformation("Emby Movies to send: {0}", embyContentMoviesToSend.Count()); + _log.LogInformation("Jellyfin Movies to send: {0}", jellyfinContentMoviesToSend.Count()); _log.LogInformation("Albums to send: {0}", lidarrContentAlbumsToSend.Count()); // Find the movies that do not yet have MovieDbIds var needsMovieDbPlex = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); var needsMovieDbEmby = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); + var needsMovieDbJellyfin = jellyfinContent.Where(x => x.Type == JellyfinMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); var newPlexMovies = await GetMoviesWithoutId(addedPlexMovieLogIds, needsMovieDbPlex); var newEmbyMovies = await GetMoviesWithoutId(addedEmbyMoviesLogIds, needsMovieDbEmby); + var newJellyfinMovies = await GetMoviesWithoutId(addedJellyfinMoviesLogIds, needsMovieDbJellyfin); plexContentMoviesToSend = plexContentMoviesToSend.Union(newPlexMovies).ToHashSet(); embyContentMoviesToSend = embyContentMoviesToSend.Union(newEmbyMovies).ToHashSet(); + jellyfinContentMoviesToSend = jellyfinContentMoviesToSend.Union(newJellyfinMovies).ToHashSet(); plexContentMoviesToSend = plexContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet(); embyContentMoviesToSend = embyContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet(); @@ -161,24 +175,30 @@ namespace Ombi.Schedule.Jobs.Ombi FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds); var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedEmbyEpisodesLogIds); + var jellyfinEpisodesToSend = FilterJellyfinEpisodes(_jellyfin.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), + addedJellyfinEpisodesLogIds); _log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count()); _log.LogInformation("Emby Episodes to send: {0}", embyEpisodesToSend.Count()); + _log.LogInformation("Jellyfin Episodes to send: {0}", jellyfinEpisodesToSend.Count()); var plexSettings = await _plexSettings.GetSettingsAsync(); var embySettings = await _embySettings.GetSettingsAsync(); + var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync(); var body = string.Empty; if (test) { var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10); var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10); + var jellyfinm = jellyfinContent.Where(x => x.Type == JellyfinMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10); 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 jellyfint = _jellyfin.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, embySettings, plexSettings); + body = await BuildHtml(plexm, embym, jellyfinm, plext, embyt, jellyfint, lidarr, settings, embySettings, jellyfinSettings, plexSettings); } else { - body = await BuildHtml(plexContentMoviesToSend.AsQueryable(), embyContentMoviesToSend.AsQueryable(), plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings, embySettings, plexSettings); + body = await BuildHtml(plexContentMoviesToSend.AsQueryable(), embyContentMoviesToSend.AsQueryable(), jellyfinContentMoviesToSend.AsQueryable(), plexEpisodesToSend, embyEpisodesToSend, jellyfinEpisodesToSend, lidarrContentAlbumsToSend, settings, embySettings, jellyfinSettings, plexSettings); if (body.IsNullOrEmpty()) { return; @@ -285,6 +305,34 @@ namespace Ombi.Schedule.Jobs.Ombi SeasonNumber = p.SeasonNumber }); } + + foreach (var e in jellyfinContentMoviesToSend) + { + if (e.Type == JellyfinMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Jellyfin, + ContentType = ContentType.Parent, + ContentId = StringHelper.IntParseLinq(e.TheMovieDbId), + }); + } + } + + foreach (var p in jellyfinEpisodesToSend) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Jellyfin, + ContentType = ContentType.Episode, + ContentId = StringHelper.IntParseLinq(p.Series.TvDbId), + EpisodeNumber = p.EpisodeNumber, + SeasonNumber = p.SeasonNumber + }); + } + await _recentlyAddedLog.AddRange(recentlyAddedLog); } else @@ -349,6 +397,20 @@ namespace Ombi.Schedule.Jobs.Ombi return result.ToHashSet(); } + private async Task> GetMoviesWithoutId(HashSet addedMovieLogIds, HashSet needsMovieDbJellyfin) + { + foreach (var movie in needsMovieDbJellyfin) + { + var id = await _refreshMetadata.GetTheMovieDbId(false, true, null, movie.ImdbId, movie.Title, true); + movie.TheMovieDbId = id.ToString(); + } + + var result = needsMovieDbJellyfin.Where(x => x.HasTheMovieDb && !addedMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))); + await UpdateTheMovieDbId(result); + // Filter them out now + return result.ToHashSet(); + } + private async Task UpdateTheMovieDbId(IEnumerable content) { foreach (var movie in content) @@ -358,6 +420,10 @@ namespace Ombi.Schedule.Jobs.Ombi continue; } var entity = await _plex.Find(movie.Id); + if (entity == null) + { + return; + } entity.TheMovieDbId = movie.TheMovieDbId; _plex.UpdateWithoutSave(entity); } @@ -379,6 +445,21 @@ namespace Ombi.Schedule.Jobs.Ombi await _plex.SaveChangesAsync(); } + private async Task UpdateTheMovieDbId(IEnumerable content) + { + foreach (var movie in content) + { + if (!movie.HasTheMovieDb) + { + continue; + } + var entity = await _jellyfin.Find(movie.Id); + entity.TheMovieDbId = movie.TheMovieDbId; + _jellyfin.UpdateWithoutSave(entity); + } + await _plex.SaveChangesAsync(); + } + public async Task Execute(IJobExecutionContext job) { var newsletterSettings = await _newsletterSettings.GetSettingsAsync(); @@ -419,6 +500,23 @@ namespace Ombi.Schedule.Jobs.Ombi return itemsToReturn; } + private HashSet FilterJellyfinEpisodes(IEnumerable source, IQueryable recentlyAdded) + { + var itemsToReturn = new HashSet(); + foreach (var ep in source.Where(x => x.Series.HasTvDb)) + { + var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId); + if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber)) + { + continue; + } + + itemsToReturn.Add(ep); + } + + return itemsToReturn; + } + private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings) { var resolver = new NotificationMessageResolver(); @@ -429,8 +527,8 @@ namespace Ombi.Schedule.Jobs.Ombi return resolver.ParseMessage(template, curlys); } - private async Task BuildHtml(IQueryable plexContentToSend, IQueryable embyContentToSend, - HashSet plexEpisodes, HashSet embyEp, HashSet albums, NewsletterSettings settings, EmbySettings embySettings, + private async Task BuildHtml(IQueryable plexContentToSend, IQueryable embyContentToSend, IQueryable jellyfinContentToSend, + HashSet plexEpisodes, HashSet embyEp, HashSet jellyfinEp, HashSet albums, NewsletterSettings settings, EmbySettings embySettings, JellyfinSettings jellyfinSettings, PlexSettings plexSettings) { var ombiSettings = await _ombiSettings.GetSettingsAsync(); @@ -438,6 +536,7 @@ namespace Ombi.Schedule.Jobs.Ombi var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie); var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie); + var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.Movie); if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies) { sb.Append("

New Movies



"); @@ -457,6 +556,11 @@ namespace Ombi.Schedule.Jobs.Ombi await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode, embySettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty); } + if (jellyfinSettings.Enable) + { + await ProcessJellyfinMovies(jellyfinMovies, sb, ombiSettings.DefaultLanguageCode, jellyfinSettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty); + } + sb.Append(""); sb.Append(""); sb.Append(""); @@ -464,7 +568,7 @@ namespace Ombi.Schedule.Jobs.Ombi sb.Append(""); } - if ((plexEpisodes.Any() || embyEp.Any()) && !settings.DisableTv) + if ((plexEpisodes.Any() || embyEp.Any()) || jellyfinEp.Any() && !settings.DisableTv) { sb.Append("

New TV



"); sb.Append( @@ -483,6 +587,11 @@ namespace Ombi.Schedule.Jobs.Ombi await ProcessEmbyTv(embyEp, sb, embySettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty); } + if (jellyfinSettings.Enable) + { + await ProcessJellyfinTv(jellyfinEp, sb, jellyfinSettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty); + } + sb.Append(""); sb.Append(""); sb.Append(""); @@ -638,6 +747,59 @@ namespace Ombi.Schedule.Jobs.Ombi } } + private async Task ProcessJellyfinMovies(IQueryable embyContent, StringBuilder sb, string defaultLangaugeCode, string customUrl) + { + int count = 0; + var ordered = embyContent.OrderByDescending(x => x.AddedAt); + foreach (var content in ordered) + { + var theMovieDbId = content.TheMovieDbId; + if (!content.TheMovieDbId.HasValue()) + { + var imdbId = content.ImdbId; + var findResult = await _movieApi.Find(imdbId, ExternalSource.imdb_id); + var result = findResult.movie_results?.FirstOrDefault(); + if (result == null) + { + continue; + } + + theMovieDbId = result.id.ToString(); + } + + var mediaurl = content.Url; + if (customUrl.HasValue()) + { + mediaurl = customUrl; + } + var info = await _movieApi.GetMovieInformationWithExtraInfo(StringHelper.IntParseLinq(theMovieDbId), defaultLangaugeCode); + if (info == null) + { + continue; + } + try + { + CreateMovieHtmlContent(sb, info, mediaurl); + count += 1; + } + catch (Exception e) + { + _log.LogError(e, "Error when processing Jellyfin Movies {0}", info.Title); + } + finally + { + EndLoopHtml(sb); + } + + if (count == 2) + { + count = 0; + sb.Append(""); + sb.Append(""); + } + } + } + private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info, string mediaurl) { AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/{info.BackdropPath}"); @@ -981,6 +1143,129 @@ namespace Ombi.Schedule.Jobs.Ombi } } + private async Task ProcessJellyfinTv(HashSet embyContent, StringBuilder sb, string serverUrl) + { + var series = new List(); + foreach (var episode in embyContent) + { + var alreadyAdded = series.FirstOrDefault(x => x.JellyfinId == episode.Series.JellyfinId); + if (alreadyAdded != null) + { + alreadyAdded.Episodes.Add(episode); + } + else + { + episode.Series.Episodes = new List + { + episode + }; + series.Add(episode.Series); + } + } + + int count = 0; + var orderedTv = series.OrderByDescending(x => x.AddedAt); + foreach (var t in orderedTv) + { + if (!t.TvDbId.HasValue()) + { + continue; + } + + int.TryParse(t.TvDbId, out var tvdbId); + var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId); + if (info == null) + { + continue; + } + + try + { + var banner = info.image?.original; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.ToHttpsUrl(); // Always use the Https banners + } + + var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId); + if (tvInfo != null && tvInfo.backdrop_path.HasValue()) + { + + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}"); + } + else + { + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/"); + } + AddPosterInsideTable(sb, banner); + AddMediaServerUrl(sb, serverUrl.HasValue() ? serverUrl : t.Url, banner); + AddInfoTable(sb); + + var title = ""; + if (!String.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4) + { + title = $"{t.Title} ({info.premiered.Remove(4)})"; + } + else + { + title = $"{t.Title}"; + } + AddTitle(sb, $"https://www.imdb.com/title/{info.externals.imdb}/", title); + + // Group by the season number + var results = t.Episodes?.GroupBy(p => p.SeasonNumber, + (key, g) => new + { + SeasonNumber = key, + Episodes = g.ToList(), + EpisodeAirDate = tvInfo?.seasons?.Where(x => x.season_number == key)?.Select(x => x.air_date).FirstOrDefault() + } + ); + + // Group the episodes + var finalsb = new StringBuilder(); + foreach (var epInformation in results.OrderBy(x => x.SeasonNumber)) + { + var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); + var episodeString = StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber)); + var episodeAirDate = epInformation.EpisodeAirDate; + finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString} {episodeAirDate}"); + finalsb.Append("
"); + } + + var summary = info.summary; + if (summary.Length > 280) + { + summary = summary.Remove(280); + summary = summary + "...

"; + } + AddTvParagraph(sb, finalsb.ToString(), summary); + + if (info.genres.Any()) + { + AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + } + + } + catch (Exception e) + { + _log.LogError(e, "Error when processing Jellyfin TV {0}", t.Title); + } + finally + { + EndLoopHtml(sb); + count += 1; + } + + if (count == 2) + { + count = 0; + sb.Append(""); + sb.Append(""); + } + } + } + private void EndLoopHtml(StringBuilder sb) { //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... @@ -1040,4 +1325,4 @@ namespace Ombi.Schedule.Jobs.Ombi GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index f7d9ae1d2..65c85b586 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TvMaze; @@ -14,6 +15,7 @@ using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Hubs; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Plex; using Ombi.Store.Entities; using Ombi.Store.Repository; @@ -23,31 +25,41 @@ namespace Ombi.Schedule.Jobs.Ombi { public class RefreshMetadata : IRefreshMetadata { - public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, + public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo, ILogger log, ITvMazeApi tvApi, ISettingsService plexSettings, - IMovieDbApi movieApi, ISettingsService embySettings, IEmbyApiFactory embyApi, IHubContext notification) + IMovieDbApi movieApi, + ISettingsService embySettings, IEmbyApiFactory embyApi, + ISettingsService jellyfinSettings, IJellyfinApiFactory jellyfinApi, + IHubContext notification) { _plexRepo = plexRepo; _embyRepo = embyRepo; + _jellyfinRepo = jellyfinRepo; _log = log; _movieApi = movieApi; _tvApi = tvApi; _plexSettings = plexSettings; _embySettings = embySettings; _embyApiFactory = embyApi; + _jellyfinSettings = jellyfinSettings; + _jellyfinApiFactory = jellyfinApi; _notification = notification; } private readonly IPlexContentRepository _plexRepo; private readonly IEmbyContentRepository _embyRepo; + private readonly IJellyfinContentRepository _jellyfinRepo; private readonly ILogger _log; private readonly IMovieDbApi _movieApi; private readonly ITvMazeApi _tvApi; private readonly ISettingsService _plexSettings; private readonly ISettingsService _embySettings; + private readonly ISettingsService _jellyfinSettings; private readonly IEmbyApiFactory _embyApiFactory; + private readonly IJellyfinApiFactory _jellyfinApiFactory; private readonly IHubContext _notification; private IEmbyApi EmbyApi { get; set; } + private IJellyfinApi JellyfinApi { get; set; } public async Task Execute(IJobExecutionContext job) { @@ -72,6 +84,14 @@ namespace Ombi.Schedule.Jobs.Ombi await OmbiQuartz.TriggerJob(nameof(IEmbyAvaliabilityChecker), "Emby"); } + + var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync(); + if (jellyfinSettings.Enable) + { + await StartJellyfin(jellyfinSettings); + + await OmbiQuartz.TriggerJob(nameof(IJellyfinAvaliabilityChecker), "Jellyfin"); + } } catch (Exception e) { @@ -107,6 +127,13 @@ namespace Ombi.Schedule.Jobs.Ombi await StartEmbyTv(); } + private async Task StartJellyfin(JellyfinSettings s) + { + JellyfinApi = _jellyfinApiFactory.CreateClient(s); + await StartJellyfinMovies(s); + await StartJellyfinTv(); + } + private async Task StartPlexTv(List allTv) { foreach (var show in allTv) @@ -178,6 +205,41 @@ namespace Ombi.Schedule.Jobs.Ombi } } + private async Task StartJellyfinTv() + { + var allTv = await _jellyfinRepo.GetAll().Where(x => + x.Type == JellyfinMediaType.Series && (x.TheMovieDbId == null || x.ImdbId == null || x.TvDbId == null)).ToListAsync(); + + foreach (var show in allTv) + { + var hasImdb = show.ImdbId.HasValue(); + var hasTheMovieDb = show.TheMovieDbId.HasValue(); + var hasTvDbId = show.TvDbId.HasValue(); + + if (!hasTheMovieDb) + { + var id = await GetTheMovieDbId(hasTvDbId, hasImdb, show.TvDbId, show.ImdbId, show.Title, false); + show.TheMovieDbId = id; + } + + if (!hasImdb) + { + var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId, RequestType.TvShow); + show.ImdbId = id; + _jellyfinRepo.UpdateWithoutSave(show); + } + + if (!hasTvDbId) + { + var id = await GetTvDbId(hasTheMovieDb, hasImdb, show.TheMovieDbId, show.ImdbId, show.Title); + show.TvDbId = id; + _jellyfinRepo.UpdateWithoutSave(show); + } + + await _jellyfinRepo.SaveChangesAsync(); + } + } + private async Task StartPlexMovies(List allMovies) { foreach (var movie in allMovies) @@ -263,6 +325,61 @@ namespace Ombi.Schedule.Jobs.Ombi } } + private async Task StartJellyfinMovies(JellyfinSettings settings) + { + var allMovies = await _jellyfinRepo.GetAll().Where(x => + x.Type == JellyfinMediaType.Movie && (x.TheMovieDbId == null || x.ImdbId == null)).ToListAsync(); + foreach (var movie in allMovies) + { + movie.ImdbId.HasValue(); + movie.TheMovieDbId.HasValue(); + // Movies don't really use TheTvDb + + // Check if it even has 1 ID + if (!movie.HasImdb && !movie.HasTheMovieDb) + { + // Ok this sucks, + // The only think I can think that has happened is that we scanned Jellyfin before Jellyfin has got the metadata + // So let's recheck jellyfin to see if they have got the metadata now + // + // Yeah your right that does suck - Future Jamie + _log.LogInformation($"Movie {movie.Title} does not have a ImdbId or TheMovieDbId, so rechecking jellyfin"); + foreach (var server in settings.Servers) + { + _log.LogInformation($"Checking server {server.Name} for upto date metadata"); + var movieInfo = await JellyfinApi.GetMovieInformation(movie.JellyfinId, 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, RequestType.Movie); + movie.ImdbId = imdbId; + _jellyfinRepo.UpdateWithoutSave(movie); + } + if (!movie.HasTheMovieDb) + { + var id = await GetTheMovieDbId(false, movie.HasImdb, string.Empty, movie.ImdbId, movie.Title, true); + movie.TheMovieDbId = id; + _jellyfinRepo.UpdateWithoutSave(movie); + } + + await _jellyfinRepo.SaveChangesAsync(); + + } + } + public async Task GetTheMovieDbId(bool hasTvDbId, bool hasImdb, string tvdbID, string imdbId, string title, bool movie) { _log.LogInformation("The Media item {0} does not have a TheMovieDbId, searching for TheMovieDbId", title); @@ -392,4 +509,4 @@ namespace Ombi.Schedule.Jobs.Ombi GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs b/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs index c80703f81..1e484b237 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs @@ -88,6 +88,12 @@ namespace Ombi.Schedule.Jobs.Plex _log.LogInformation("Could not create Plex user since the have no username, PlexUserId: {0}", plexUser.Id); continue; } + + if ((plexUser.Email.HasValue()) && await _userManager.FindByEmailAsync(plexUser.Email) != null) + { + _log.LogWarning($"Cannot add user {plexUser.Username} because their email address is already in Ombi, skipping this user"); + continue; + } // Create this users // We do not store a password against the user since they will authenticate via Plex var newUser = new OmbiUser @@ -98,7 +104,8 @@ namespace Ombi.Schedule.Jobs.Plex Email = plexUser?.Email ?? string.Empty, Alias = string.Empty, MovieRequestLimit = userManagementSettings.MovieRequestLimit, - EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit + EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit, + StreamingCountry = userManagementSettings.DefaultStreamingCountry }; _log.LogInformation("Creating Plex user {0}", newUser.UserName); var result = await _userManager.CreateAsync(newUser); @@ -161,7 +168,8 @@ namespace Ombi.Schedule.Jobs.Plex UserName = plexAdmin.username ?? plexAdmin.id, ProviderUserId = plexAdmin.id, Email = plexAdmin.email ?? string.Empty, - Alias = string.Empty + Alias = string.Empty, + StreamingCountry = settings.DefaultStreamingCountry }; var result = await _userManager.CreateAsync(newUser); diff --git a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs index 3657fba61..ed867fd8b 100644 --- a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs @@ -46,12 +46,16 @@ namespace Ombi.Schedule.Jobs.Radarr var movies = await RadarrApi.GetMovies(settings.ApiKey, settings.FullUri); if (movies != null) { - // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM RadarrCache"); - tran.Commit(); - } + // Let's remove the old cached data + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM RadarrCache"); + tran.Commit(); + } + }); var movieIds = new List(); foreach (var m in movies) @@ -72,14 +76,17 @@ namespace Ombi.Schedule.Jobs.Radarr } } } - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.RadarrCache.AddRangeAsync(movieIds); + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.RadarrCache.AddRangeAsync(movieIds); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); } await OmbiQuartz.TriggerJob(nameof(IArrAvailabilityChecker), "DVR"); diff --git a/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs b/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs index a223a0cfa..91920dcd8 100644 --- a/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs +++ b/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs @@ -45,12 +45,16 @@ namespace Ombi.Schedule.Jobs.SickRage if (shows != null) { var srShows = shows.data.Values; - var ids = srShows.Select(x => x.tvdbid); - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var ids = srShows.Select(x => x.tvdbid); + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SickRageCache"); - tran.Commit(); - } + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SickRageCache"); + tran.Commit(); + } + }); var entites = ids.Select(id => new SickRageCache { TvDbId = id }).ToList(); @@ -77,13 +81,16 @@ namespace Ombi.Schedule.Jobs.SickRage } } - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.SickRageEpisodeCache.AddRangeAsync(episodesToAdd); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.SickRageEpisodeCache.AddRangeAsync(episodesToAdd); + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); } } catch (Exception e) diff --git a/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs b/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs index bf3ee8406..82c699598 100644 --- a/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs @@ -48,12 +48,16 @@ namespace Ombi.Schedule.Jobs.Sonarr if (series != null) { var sonarrSeries = series as ImmutableHashSet ?? series.ToImmutableHashSet(); - var ids = sonarrSeries.Select(x => x.tvdbId); - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var ids = sonarrSeries.Select(x => x.tvdbId); + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SonarrCache"); - tran.Commit(); - } + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SonarrCache"); + tran.Commit(); + } + }); var existingSeries = await _ctx.SonarrCache.Select(x => x.TvDbId).ToListAsync(); @@ -62,11 +66,15 @@ namespace Ombi.Schedule.Jobs.Sonarr await _ctx.SonarrCache.AddRangeAsync(entites); entites.Clear(); - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SonarrEpisodeCache"); - tran.Commit(); - } + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SonarrEpisodeCache"); + tran.Commit(); + } + }); foreach (var s in sonarrSeries) { @@ -116,14 +124,17 @@ namespace Ombi.Schedule.Jobs.Sonarr // } //} - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.SonarrEpisodeCache.AddRangeAsync(episodesToAdd); - _log.LogDebug("Commiting the transaction"); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.SonarrEpisodeCache.AddRangeAsync(episodesToAdd); + _log.LogDebug("Commiting the transaction"); + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); } } diff --git a/src/Ombi.Schedule/Ombi.Schedule.csproj b/src/Ombi.Schedule/Ombi.Schedule.csproj index ac63d5b2f..8f26d32f2 100644 --- a/src/Ombi.Schedule/Ombi.Schedule.csproj +++ b/src/Ombi.Schedule/Ombi.Schedule.csproj @@ -23,6 +23,7 @@ + @@ -37,4 +38,4 @@ - \ No newline at end of file + diff --git a/src/Ombi.Schedule/OmbiScheduler.cs b/src/Ombi.Schedule/OmbiScheduler.cs index 4a91052c7..6c54895d6 100644 --- a/src/Ombi.Schedule/OmbiScheduler.cs +++ b/src/Ombi.Schedule/OmbiScheduler.cs @@ -7,6 +7,7 @@ using Ombi.Helpers; using Ombi.Schedule.Jobs; using Ombi.Schedule.Jobs.Couchpotato; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Lidarr; using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Plex; @@ -51,6 +52,7 @@ namespace Ombi.Schedule // Run configuration await AddPlex(s); await AddEmby(s); + await AddJellyfin(s); await AddDvrApps(s); await AddSystem(s); await AddNotifications(s); @@ -98,9 +100,18 @@ namespace Ombi.Schedule await OmbiQuartz.Instance.AddJob(nameof(IEmbyAvaliabilityChecker), "Emby", null); await OmbiQuartz.Instance.AddJob(nameof(IEmbyUserImporter), "Emby", JobSettingsHelper.UserImporter(s)); } + + private static async Task AddJellyfin(JobSettings s) + { + await OmbiQuartz.Instance.AddJob(nameof(IJellyfinContentSync), "Jellyfin", JobSettingsHelper.JellyfinContent(s)); + await OmbiQuartz.Instance.AddJob(nameof(IJellyfinEpisodeSync), "Jellyfin", null); + await OmbiQuartz.Instance.AddJob(nameof(IJellyfinAvaliabilityChecker), "Jellyfin", null); + await OmbiQuartz.Instance.AddJob(nameof(IJellyfinUserImporter), "Jellyfin", JobSettingsHelper.UserImporter(s)); + } + private static async Task AddNotifications(JobSettings s) { await OmbiQuartz.Instance.AddJob(nameof(INotificationService), "Notifications", null); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs index b3ffce0e1..5bd7cea93 100644 --- a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs @@ -6,7 +6,6 @@ namespace Ombi.Core.Settings.Models.External public sealed class EmbySettings : Ombi.Settings.Settings.Models.Settings { public bool Enable { get; set; } - public bool IsJellyfin { get; set; } public List Servers { get; set; } = new List(); } @@ -19,4 +18,4 @@ namespace Ombi.Core.Settings.Models.External public string ServerHostname { get; set; } public bool EnableEpisodeSearching { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs new file mode 100644 index 000000000..3bee56848 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.External; + +namespace Ombi.Core.Settings.Models.External +{ + public sealed class JellyfinSettings : Ombi.Settings.Settings.Models.Settings + { + public bool Enable { get; set; } + public List Servers { get; set; } = new List(); + } + + public class JellyfinServers : ExternalSettings + { + public string ServerId { get; set; } + public string Name { get; set; } + public string ApiKey { get; set; } + public string AdministratorId { get; set; } + public string ServerHostname { get; set; } + public bool EnableEpisodeSearching { get; set; } + } +} diff --git a/src/Ombi.Settings/Settings/Models/JobSettings.cs b/src/Ombi.Settings/Settings/Models/JobSettings.cs index 9bdad5e2b..18bde6774 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettings.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettings.cs @@ -3,6 +3,7 @@ public class JobSettings : Settings { public string EmbyContentSync { get; set; } + public string JellyfinContentSync { get; set; } public string SonarrSync { get; set; } public string RadarrSync { get; set; } public string PlexContentSync { get; set; } @@ -18,4 +19,4 @@ public string MediaDatabaseRefresh { get; set; } public string AutoDeleteRequests { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs index 2778ba478..fef0792d3 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs @@ -21,6 +21,11 @@ namespace Ombi.Settings.Settings.Models return ValidateCron(Get(s.EmbyContentSync, Cron.Hourly(5))); } + public static string JellyfinContent(JobSettings s) + { + return ValidateCron(Get(s.JellyfinContentSync, Cron.Hourly(5))); + } + public static string PlexContent(JobSettings s) { return ValidateCron(Get(s.PlexContentSync, Cron.Daily(2))); @@ -97,4 +102,4 @@ namespace Ombi.Settings.Settings.Models return _defaultCron; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Settings/Settings/Models/UserManagementSettings.cs b/src/Ombi.Settings/Settings/Models/UserManagementSettings.cs index 17002a2a7..ff6cff278 100644 --- a/src/Ombi.Settings/Settings/Models/UserManagementSettings.cs +++ b/src/Ombi.Settings/Settings/Models/UserManagementSettings.cs @@ -7,10 +7,13 @@ namespace Ombi.Settings.Settings.Models public bool ImportPlexAdmin { get; set; } public bool ImportPlexUsers { get; set; } public bool ImportEmbyUsers { get; set; } + public bool ImportJellyfinUsers { get; set; } public int MovieRequestLimit { get; set; } public int EpisodeRequestLimit { get; set; } + public string DefaultStreamingCountry { get; set; } = "US"; public List DefaultRoles { get; set; } = new List(); public List BannedPlexUserIds { get; set; } = new List(); public List BannedEmbyUserIds { get; set; } = new List(); + public List BannedJellyfinUserIds { get; set; } = new List(); } -} \ No newline at end of file +} diff --git a/src/Ombi.Store/Context/ExternalContext.cs b/src/Ombi.Store/Context/ExternalContext.cs index 99ec4cb5f..ce121fe10 100644 --- a/src/Ombi.Store/Context/ExternalContext.cs +++ b/src/Ombi.Store/Context/ExternalContext.cs @@ -30,6 +30,8 @@ namespace Ombi.Store.Context public DbSet CouchPotatoCache { get; set; } public DbSet EmbyContent { get; set; } public DbSet EmbyEpisode { get; set; } + public DbSet JellyfinEpisode { get; set; } + public DbSet JellyfinContent { get; set; } public DbSet SonarrCache { get; set; } public DbSet LidarrArtistCache { get; set; } @@ -51,7 +53,13 @@ namespace Ombi.Store.Context .HasPrincipalKey(x => x.EmbyId) .HasForeignKey(p => p.ParentId); + builder.Entity() + .HasOne(p => p.Series) + .WithMany(b => b.Episodes) + .HasPrincipalKey(x => x.JellyfinId) + .HasForeignKey(p => p.ParentId); + base.OnModelCreating(builder); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 90e559875..3b698a47f 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -57,23 +57,27 @@ namespace Ombi.Store.Context public void Seed() { - - using (var tran = Database.BeginTransaction()) + var strat = Database.CreateExecutionStrategy(); + strat.Execute(() => { - // Make sure we have the API User - var apiUserExists = Users.ToList().Any(x => x.NormalizedUserName == "API"); - if (!apiUserExists) + using (var tran = Database.BeginTransaction()) { - Users.Add(new OmbiUser + // Make sure we have the API User + var apiUserExists = Users.ToList().Any(x => x.NormalizedUserName == "API"); + if (!apiUserExists) { - UserName = "Api", - UserType = UserType.SystemUser, - NormalizedUserName = "API", - }); - SaveChanges(); - tran.Commit(); + Users.Add(new OmbiUser + { + UserName = "Api", + UserType = UserType.SystemUser, + NormalizedUserName = "API", + StreamingCountry = "US" + }); + SaveChanges(); + tran.Commit(); + } } - } + }); //Check if templates exist var templates = NotificationTemplates.ToList(); @@ -216,12 +220,14 @@ namespace Ombi.Store.Context if (needToSave) { - - using (var tran = Database.BeginTransaction()) + strat.Execute(() => { - SaveChanges(); - tran.Commit(); - } + using (var tran = Database.BeginTransaction()) + { + SaveChanges(); + tran.Commit(); + } + }); } } } diff --git a/src/Ombi.Store/Context/SettingsContext.cs b/src/Ombi.Store/Context/SettingsContext.cs index 9f6d52791..83ffa792d 100644 --- a/src/Ombi.Store/Context/SettingsContext.cs +++ b/src/Ombi.Store/Context/SettingsContext.cs @@ -29,44 +29,48 @@ namespace Ombi.Store.Context public void Seed() { - using (var tran = Database.BeginTransaction()) + var strat = Database.CreateExecutionStrategy(); + strat.Execute(() => { - // Add the tokens - var fanArt = ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.FanartTv); - if (fanArt == null) + using (var tran = Database.BeginTransaction()) { - ApplicationConfigurations.Add(new ApplicationConfiguration + // Add the tokens + var fanArt = ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.FanartTv); + if (fanArt == null) { - Type = ConfigurationTypes.FanartTv, - Value = "4b6d983efa54d8f45c68432521335f15" - }); - SaveChanges(); - } + ApplicationConfigurations.Add(new ApplicationConfiguration + { + Type = ConfigurationTypes.FanartTv, + Value = "4b6d983efa54d8f45c68432521335f15" + }); + SaveChanges(); + } - var movieDb = ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.FanartTv); - if (movieDb == null) - { - ApplicationConfigurations.Add(new ApplicationConfiguration + var movieDb = ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.FanartTv); + if (movieDb == null) { - Type = ConfigurationTypes.TheMovieDb, - Value = "b8eabaf5608b88d0298aa189dd90bf00" - }); - SaveChanges(); - } + ApplicationConfigurations.Add(new ApplicationConfiguration + { + Type = ConfigurationTypes.TheMovieDb, + Value = "b8eabaf5608b88d0298aa189dd90bf00" + }); + SaveChanges(); + } - var notification = - ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.Notification); - if (notification == null) - { - ApplicationConfigurations.Add(new ApplicationConfiguration + var notification = + ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.Notification); + if (notification == null) { - Type = ConfigurationTypes.Notification, - Value = "4f0260c4-9c3d-41ab-8d68-27cb5a593f0e" - }); - SaveChanges(); + ApplicationConfigurations.Add(new ApplicationConfiguration + { + Type = ConfigurationTypes.Notification, + Value = "4f0260c4-9c3d-41ab-8d68-27cb5a593f0e" + }); + SaveChanges(); + } + tran.Commit(); } - tran.Commit(); - } + }); } } } \ No newline at end of file diff --git a/src/Ombi.Store/Entities/JellyfinContent.cs b/src/Ombi.Store/Entities/JellyfinContent.cs new file mode 100644 index 000000000..857457bde --- /dev/null +++ b/src/Ombi.Store/Entities/JellyfinContent.cs @@ -0,0 +1,71 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinContent.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ombi.Store.Entities +{ + [Table("JellyfinContent")] + public class JellyfinContent : Entity + { + public string Title { get; set; } + + /// + /// OBSOLETE, Cannot delete due to DB migration issues with SQLite + /// + public string ProviderId { get; set; } + public string JellyfinId { get; set; } + public JellyfinMediaType Type { get; set; } + public DateTime AddedAt { get; set; } + + public string ImdbId { get; set; } + public string TheMovieDbId { get; set; } + public string TvDbId { get; set; } + + public string Url { get; set; } + + public ICollection Episodes { get; set; } + + [NotMapped] + public bool HasImdb => !string.IsNullOrEmpty(ImdbId); + + [NotMapped] + public bool HasTvDb => !string.IsNullOrEmpty(TvDbId); + + [NotMapped] + public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId); + } + + public enum JellyfinMediaType + { + Movie = 0, + Series = 1, + Music = 2 + } +} diff --git a/src/Ombi.Store/Entities/JellyfinEpisode.cs b/src/Ombi.Store/Entities/JellyfinEpisode.cs new file mode 100644 index 000000000..f2c2f820d --- /dev/null +++ b/src/Ombi.Store/Entities/JellyfinEpisode.cs @@ -0,0 +1,53 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinEpisode.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Ombi.Store.Entities +{ + [Table("JellyfinEpisode")] + public class JellyfinEpisode : Entity + { + public string Title { get; set; } + public string JellyfinId { get; set; } + public int EpisodeNumber { get; set; } + public int SeasonNumber { get; set; } + public string ParentId { get; set; } + /// + /// NOT USED + /// + public string ProviderId { get; set; } + public DateTime AddedAt { get; set; } + public string TvDbId { get; set; } + public string ImdbId { get; set; } + public string TheMovieDbId { get; set; } + + public JellyfinContent Series { get; set; } + } +} diff --git a/src/Ombi.Store/Entities/OmbiUser.cs b/src/Ombi.Store/Entities/OmbiUser.cs index 919d9a22c..46a49b1ae 100644 --- a/src/Ombi.Store/Entities/OmbiUser.cs +++ b/src/Ombi.Store/Entities/OmbiUser.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.AspNetCore.Identity; using Newtonsoft.Json; -using Ombi.Helpers; namespace Ombi.Store.Entities { @@ -21,6 +21,12 @@ namespace Ombi.Store.Entities public string Language { get; set; } + /// + /// Used to get the Streaming information for media + /// + [Required] + public string StreamingCountry { get; set; } + public int? MovieRequestLimit { get; set; } public int? EpisodeRequestLimit { get; set; } public int? MusicRequestLimit { get; set; } @@ -40,14 +46,14 @@ namespace Ombi.Store.Entities public bool EmailLogin { get; set; } [NotMapped] public bool IsSystemUser => UserType == UserType.SystemUser; - + [JsonIgnore] public override string PasswordHash { get => base.PasswordHash; set => base.PasswordHash = value; } - + [JsonIgnore] public override string SecurityStamp { diff --git a/src/Ombi.Store/Entities/RecentlyAddedLog.cs b/src/Ombi.Store/Entities/RecentlyAddedLog.cs index 782d89e3f..af6012f58 100644 --- a/src/Ombi.Store/Entities/RecentlyAddedLog.cs +++ b/src/Ombi.Store/Entities/RecentlyAddedLog.cs @@ -19,7 +19,8 @@ namespace Ombi.Store.Entities { Plex = 0, Emby = 1, - Lidarr = 2 + Lidarr = 2, + Jellyfin = 3 } public enum ContentType @@ -28,4 +29,4 @@ namespace Ombi.Store.Entities Episode = 1, Album = 2, } -} \ No newline at end of file +} diff --git a/src/Ombi.Store/Entities/Requests/SeasonRequests.cs b/src/Ombi.Store/Entities/Requests/SeasonRequests.cs index b33634db1..8a69a4767 100644 --- a/src/Ombi.Store/Entities/Requests/SeasonRequests.cs +++ b/src/Ombi.Store/Entities/Requests/SeasonRequests.cs @@ -50,7 +50,7 @@ namespace Ombi.Store.Repository.Requests return "Common.ProcessingRequest"; } - if (!Approved && !Available) + if (!Approved && !Available && Requested) { return "Common.PendingApproval"; } diff --git a/src/Ombi.Store/Entities/User.cs b/src/Ombi.Store/Entities/User.cs index b53af5a33..0a3d54970 100644 --- a/src/Ombi.Store/Entities/User.cs +++ b/src/Ombi.Store/Entities/User.cs @@ -34,5 +34,6 @@ namespace Ombi.Store.Entities PlexUser = 2, EmbyUser = 3, EmbyConnectUser = 4, + JellyfinUser = 5, } -} \ No newline at end of file +} diff --git a/src/Ombi.Store/Migration.md b/src/Ombi.Store/Migration.md new file mode 100644 index 000000000..6225229a3 --- /dev/null +++ b/src/Ombi.Store/Migration.md @@ -0,0 +1,29 @@ +``` +dotnet ef migrations add Inital --context OmbiSqliteContext --startup-project ../Ombi/Ombi.csproj +``` + +If running migrations for any db provider other than Sqlite, then ensure the database.json is pointing at the correct DB type + + +## More detailed explanation + +1. Install dotnet-ef, and include it in your $PATH if necessary: + + ``` + dotnet tool install --global dotnet-ef + export PATH="$HOME/.dotnet/tools:$PATH" + ``` + +1. List the available `dbcontext`s, and select the one that matches the database your fields will go in: + + ``` + cd src/Ombi.Store + dotnet ef dbcontext list + ``` + +1. Run the migration using the command at the start of this document: + + ``` + cd src/Ombi.Store + dotnet ef migrations add --context --startup-project ../Ombi/Ombi.csproj + ``` diff --git a/src/Ombi.Store/Migration.txt b/src/Ombi.Store/Migration.txt deleted file mode 100644 index 331299143..000000000 --- a/src/Ombi.Store/Migration.txt +++ /dev/null @@ -1,3 +0,0 @@ -dotnet ef migrations add Inital --context OmbiSqliteContext --startup-project ../Ombi/Ombi.csproj - -If running migrations for any db provider other than Sqlite, then ensure the database.json is pointing at the correct DB type \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.Designer.cs new file mode 100644 index 000000000..72f8d2376 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.Designer.cs @@ -0,0 +1,507 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.MySql; + +namespace Ombi.Store.Migrations.ExternalMySql +{ + [DbContext(typeof(ExternalMySqlContext))] + [Migration("20210103205509_Jellyfin")] + partial class Jellyfin + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .HasColumnType("longtext"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ForeignAlbumId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.Property("PercentOfTracks") + .HasColumnType("decimal(65,30)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TrackCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ArtistName") + .HasColumnType("longtext"); + + b.Property("ForeignArtistId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("GrandparentKey") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("int"); + + b.Property("ParentKey") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ParentKey") + .HasColumnType("int"); + + b.Property("PlexContentId") + .HasColumnType("int"); + + b.Property("PlexServerContentId") + .HasColumnType("int"); + + b.Property("SeasonKey") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("int"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("ReleaseYear") + .HasColumnType("longtext"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.cs new file mode 100644 index 000000000..cd7fe85d5 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.cs @@ -0,0 +1,76 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ombi.Store.Migrations.ExternalMySql +{ + public partial class Jellyfin : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "JellyfinContent", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(type: "longtext", nullable: true), + ProviderId = table.Column(type: "longtext", nullable: true), + JellyfinId = table.Column(type: "varchar(255)", nullable: false), + Type = table.Column(type: "int", nullable: false), + AddedAt = table.Column(type: "datetime(6)", nullable: false), + ImdbId = table.Column(type: "longtext", nullable: true), + TheMovieDbId = table.Column(type: "longtext", nullable: true), + TvDbId = table.Column(type: "longtext", nullable: true), + Url = table.Column(type: "longtext", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JellyfinContent", x => x.Id); + table.UniqueConstraint("AK_JellyfinContent_JellyfinId", x => x.JellyfinId); + }); + + migrationBuilder.CreateTable( + name: "JellyfinEpisode", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(type: "longtext", nullable: true), + JellyfinId = table.Column(type: "longtext", nullable: true), + EpisodeNumber = table.Column(type: "int", nullable: false), + SeasonNumber = table.Column(type: "int", nullable: false), + ParentId = table.Column(type: "varchar(255)", nullable: true), + ProviderId = table.Column(type: "longtext", nullable: true), + AddedAt = table.Column(type: "datetime(6)", nullable: false), + TvDbId = table.Column(type: "longtext", nullable: true), + ImdbId = table.Column(type: "longtext", nullable: true), + TheMovieDbId = table.Column(type: "longtext", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JellyfinEpisode", x => x.Id); + table.ForeignKey( + name: "FK_JellyfinEpisode_JellyfinContent_ParentId", + column: x => x.ParentId, + principalTable: "JellyfinContent", + principalColumn: "JellyfinId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_JellyfinEpisode_ParentId", + table: "JellyfinEpisode", + column: "ParentId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "JellyfinEpisode"); + + migrationBuilder.DropTable( + name: "JellyfinContent"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs index 890e24b76..00b274c52 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs @@ -14,8 +14,8 @@ namespace Ombi.Store.Migrations.ExternalMySql { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.1") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.1"); modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => { @@ -42,28 +42,28 @@ namespace Ombi.Store.Migrations.ExternalMySql b.Property("EmbyId") .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(255)"); b.Property("ImdbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("ProviderId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("TheMovieDbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Type") .HasColumnType("int"); b.Property("Url") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -80,31 +80,31 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("datetime(6)"); b.Property("EmbyId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("EpisodeNumber") .HasColumnType("int"); b.Property("ImdbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("ParentId") - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(255)"); b.Property("ProviderId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("SeasonNumber") .HasColumnType("int"); b.Property("TheMovieDbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -113,6 +113,88 @@ namespace Ombi.Store.Migrations.ExternalMySql b.ToTable("EmbyEpisode"); }); + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => { b.Property("Id") @@ -126,7 +208,7 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("int"); b.Property("ForeignAlbumId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Monitored") .HasColumnType("tinyint(1)"); @@ -138,7 +220,7 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("datetime(6)"); b.Property("Title") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("TrackCount") .HasColumnType("int"); @@ -158,10 +240,10 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("int"); b.Property("ArtistName") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("ForeignArtistId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Monitored") .HasColumnType("tinyint(1)"); @@ -193,7 +275,7 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("int"); b.Property("Title") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -240,34 +322,34 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("datetime(6)"); b.Property("ImdbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Key") .HasColumnType("int"); b.Property("Quality") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("ReleaseYear") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("RequestId") .HasColumnType("int"); b.Property("TheMovieDbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Type") .HasColumnType("int"); b.Property("Url") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -368,6 +450,18 @@ namespace Ombi.Store.Migrations.ExternalMySql .WithMany("Episodes") .HasForeignKey("ParentId") .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); }); modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => @@ -378,6 +472,8 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasPrincipalKey("Key") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Series"); }); modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => @@ -386,6 +482,23 @@ namespace Ombi.Store.Migrations.ExternalMySql .WithMany("Seasons") .HasForeignKey("PlexServerContentId"); }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.Designer.cs new file mode 100644 index 000000000..19189c2e0 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.Sqlite; + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + [DbContext(typeof(ExternalSqliteContext))] + [Migration("20201212014227_Jellyfin")] + partial class Jellyfin + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ForeignAlbumId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.Property("PercentOfTracks") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ArtistName") + .HasColumnType("TEXT"); + + b.Property("ForeignArtistId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("GrandparentKey") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("ParentKey") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ParentKey") + .HasColumnType("INTEGER"); + + b.Property("PlexContentId") + .HasColumnType("INTEGER"); + + b.Property("PlexServerContentId") + .HasColumnType("INTEGER"); + + b.Property("SeasonKey") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("ReleaseYear") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs new file mode 100644 index 000000000..cd6d225d5 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs @@ -0,0 +1,76 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + public partial class Jellyfin : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "JellyfinContent", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", nullable: true), + ProviderId = table.Column(type: "TEXT", nullable: true), + JellyfinId = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + AddedAt = table.Column(type: "TEXT", nullable: false), + ImdbId = table.Column(type: "TEXT", nullable: true), + TheMovieDbId = table.Column(type: "TEXT", nullable: true), + TvDbId = table.Column(type: "TEXT", nullable: true), + Url = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JellyfinContent", x => x.Id); + table.UniqueConstraint("AK_JellyfinContent_JellyfinId", x => x.JellyfinId); + }); + + migrationBuilder.CreateTable( + name: "JellyfinEpisode", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", nullable: true), + JellyfinId = table.Column(type: "TEXT", nullable: true), + EpisodeNumber = table.Column(type: "INTEGER", nullable: false), + SeasonNumber = table.Column(type: "INTEGER", nullable: false), + ParentId = table.Column(type: "TEXT", nullable: true), + ProviderId = table.Column(type: "TEXT", nullable: true), + AddedAt = table.Column(type: "TEXT", nullable: false), + TvDbId = table.Column(type: "TEXT", nullable: true), + ImdbId = table.Column(type: "TEXT", nullable: true), + TheMovieDbId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JellyfinEpisode", x => x.Id); + table.ForeignKey( + name: "FK_JellyfinEpisode_JellyfinContent_ParentId", + column: x => x.ParentId, + principalTable: "JellyfinContent", + principalColumn: "JellyfinId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_JellyfinEpisode_ParentId", + table: "JellyfinEpisode", + column: "ParentId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "JellyfinContent"); + + migrationBuilder.DropTable( + name: "JellyfinEpisode"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs index 86e292e2f..d8378c814 100644 --- a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs @@ -14,14 +14,16 @@ namespace Ombi.Store.Migrations.ExternalSqlite { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + .HasAnnotation("ProductVersion", "5.0.1"); modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("TheMovieDbId"); + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -31,26 +33,36 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AddedAt"); + b.Property("AddedAt") + .HasColumnType("TEXT"); b.Property("EmbyId") - .IsRequired(); + .IsRequired() + .HasColumnType("TEXT"); - b.Property("ImdbId"); + b.Property("ImdbId") + .HasColumnType("TEXT"); - b.Property("ProviderId"); + b.Property("ProviderId") + .HasColumnType("TEXT"); - b.Property("TheMovieDbId"); + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); - b.Property("Title"); + b.Property("Title") + .HasColumnType("TEXT"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("TEXT"); - b.Property("Type"); + b.Property("Type") + .HasColumnType("INTEGER"); - b.Property("Url"); + b.Property("Url") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -60,27 +72,38 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AddedAt"); + b.Property("AddedAt") + .HasColumnType("TEXT"); - b.Property("EmbyId"); + b.Property("EmbyId") + .HasColumnType("TEXT"); - b.Property("EpisodeNumber"); + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); - b.Property("ImdbId"); + b.Property("ImdbId") + .HasColumnType("TEXT"); - b.Property("ParentId"); + b.Property("ParentId") + .HasColumnType("TEXT"); - b.Property("ProviderId"); + b.Property("ProviderId") + .HasColumnType("TEXT"); - b.Property("SeasonNumber"); + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); - b.Property("TheMovieDbId"); + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); - b.Property("Title"); + b.Property("Title") + .HasColumnType("TEXT"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -89,26 +112,117 @@ namespace Ombi.Store.Migrations.ExternalSqlite b.ToTable("EmbyEpisode"); }); + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AddedAt"); + b.Property("AddedAt") + .HasColumnType("TEXT"); - b.Property("ArtistId"); + b.Property("ArtistId") + .HasColumnType("INTEGER"); - b.Property("ForeignAlbumId"); + b.Property("ForeignAlbumId") + .HasColumnType("TEXT"); - b.Property("Monitored"); + b.Property("Monitored") + .HasColumnType("INTEGER"); - b.Property("PercentOfTracks"); + b.Property("PercentOfTracks") + .HasColumnType("TEXT"); - b.Property("ReleaseDate"); + b.Property("ReleaseDate") + .HasColumnType("TEXT"); - b.Property("Title"); + b.Property("Title") + .HasColumnType("TEXT"); - b.Property("TrackCount"); + b.Property("TrackCount") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -118,15 +232,20 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ArtistId"); + b.Property("ArtistId") + .HasColumnType("INTEGER"); - b.Property("ArtistName"); + b.Property("ArtistName") + .HasColumnType("TEXT"); - b.Property("ForeignArtistId"); + b.Property("ForeignArtistId") + .HasColumnType("TEXT"); - b.Property("Monitored"); + b.Property("Monitored") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -136,19 +255,26 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("EpisodeNumber"); + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); - b.Property("GrandparentKey"); + b.Property("GrandparentKey") + .HasColumnType("INTEGER"); - b.Property("Key"); + b.Property("Key") + .HasColumnType("INTEGER"); - b.Property("ParentKey"); + b.Property("ParentKey") + .HasColumnType("INTEGER"); - b.Property("SeasonNumber"); + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); - b.Property("Title"); + b.Property("Title") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -160,17 +286,23 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ParentKey"); + b.Property("ParentKey") + .HasColumnType("INTEGER"); - b.Property("PlexContentId"); + b.Property("PlexContentId") + .HasColumnType("INTEGER"); - b.Property("PlexServerContentId"); + b.Property("PlexServerContentId") + .HasColumnType("INTEGER"); - b.Property("SeasonKey"); + b.Property("SeasonKey") + .HasColumnType("INTEGER"); - b.Property("SeasonNumber"); + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -182,29 +314,41 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AddedAt"); + b.Property("AddedAt") + .HasColumnType("TEXT"); - b.Property("ImdbId"); + b.Property("ImdbId") + .HasColumnType("TEXT"); - b.Property("Key"); + b.Property("Key") + .HasColumnType("INTEGER"); - b.Property("Quality"); + b.Property("Quality") + .HasColumnType("TEXT"); - b.Property("ReleaseYear"); + b.Property("ReleaseYear") + .HasColumnType("TEXT"); - b.Property("RequestId"); + b.Property("RequestId") + .HasColumnType("INTEGER"); - b.Property("TheMovieDbId"); + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); - b.Property("Title"); + b.Property("Title") + .HasColumnType("TEXT"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("TEXT"); - b.Property("Type"); + b.Property("Type") + .HasColumnType("INTEGER"); - b.Property("Url"); + b.Property("Url") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -214,11 +358,14 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("HasFile"); + b.Property("HasFile") + .HasColumnType("INTEGER"); - b.Property("TheMovieDbId"); + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -228,9 +375,11 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -240,13 +389,17 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("EpisodeNumber"); + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); - b.Property("SeasonNumber"); + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -256,9 +409,11 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -268,15 +423,20 @@ namespace Ombi.Store.Migrations.ExternalSqlite modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("EpisodeNumber"); + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); - b.Property("HasFile"); + b.Property("HasFile") + .HasColumnType("INTEGER"); - b.Property("SeasonNumber"); + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); - b.Property("TvDbId"); + b.Property("TvDbId") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -289,6 +449,18 @@ namespace Ombi.Store.Migrations.ExternalSqlite .WithMany("Episodes") .HasForeignKey("ParentId") .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); }); modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => @@ -297,15 +469,35 @@ namespace Ombi.Store.Migrations.ExternalSqlite .WithMany("Episodes") .HasForeignKey("GrandparentKey") .HasPrincipalKey("Key") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); }); modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => { - b.HasOne("Ombi.Store.Entities.PlexServerContent") + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) .WithMany("Seasons") .HasForeignKey("PlexServerContentId"); }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Ombi.Store/Migrations/OmbiMySql/20210106132735_UserStreamingCountry.Designer.cs b/src/Ombi.Store/Migrations/OmbiMySql/20210106132735_UserStreamingCountry.Designer.cs new file mode 100644 index 000000000..989ba1ba0 --- /dev/null +++ b/src/Ombi.Store/Migrations/OmbiMySql/20210106132735_UserStreamingCountry.Designer.cs @@ -0,0 +1,1225 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.MySql; + +namespace Ombi.Store.Migrations.OmbiMySql +{ + [DbContext(typeof(OmbiMySqlContext))] + [Migration("20210106132735_UserStreamingCountry")] + partial class UserStreamingCountry + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AuditArea") + .HasColumnType("int"); + + b.Property("AuditType") + .HasColumnType("int"); + + b.Property("DateTime") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("User") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.MobileDevices", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("MobileDevices"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Agent") + .HasColumnType("int"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("PlayerId") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Alias") + .HasColumnType("longtext"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("EpisodeRequestLimit") + .HasColumnType("int"); + + b.Property("Language") + .HasColumnType("longtext"); + + b.Property("LastLoggedIn") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("MovieRequestLimit") + .HasColumnType("int"); + + b.Property("MusicRequestLimit") + .HasColumnType("int"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("ProviderUserId") + .HasColumnType("longtext"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("StreamingCountry") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserAccessToken") + .HasColumnType("longtext"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("AlbumId") + .HasColumnType("longtext"); + + b.Property("ContentId") + .HasColumnType("int"); + + b.Property("ContentType") + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestQueue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Completed") + .HasColumnType("datetime(6)"); + + b.Property("Dts") + .HasColumnType("datetime(6)"); + + b.Property("Error") + .HasColumnType("longtext"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RequestQueue"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestSubscription"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("ArtistName") + .HasColumnType("longtext"); + + b.Property("Available") + .HasColumnType("tinyint(1)"); + + b.Property("Cover") + .HasColumnType("longtext"); + + b.Property("Denied") + .HasColumnType("tinyint(1)"); + + b.Property("DeniedReason") + .HasColumnType("longtext"); + + b.Property("Disk") + .HasColumnType("longtext"); + + b.Property("ForeignAlbumId") + .HasColumnType("longtext"); + + b.Property("ForeignArtistId") + .HasColumnType("longtext"); + + b.Property("MarkedAsApproved") + .HasColumnType("datetime(6)"); + + b.Property("MarkedAsAvailable") + .HasColumnType("datetime(6)"); + + b.Property("MarkedAsDenied") + .HasColumnType("datetime(6)"); + + b.Property("Rating") + .HasColumnType("decimal(65,30)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("RequestedByAlias") + .HasColumnType("longtext"); + + b.Property("RequestedDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestedUserId") + .HasColumnType("varchar(255)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("AlbumRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("Available") + .HasColumnType("tinyint(1)"); + + b.Property("Denied") + .HasColumnType("tinyint(1)"); + + b.Property("DeniedReason") + .HasColumnType("longtext"); + + b.Property("IssueId") + .HasColumnType("int"); + + b.Property("MarkedAsApproved") + .HasColumnType("datetime(6)"); + + b.Property("MarkedAsAvailable") + .HasColumnType("datetime(6)"); + + b.Property("MarkedAsDenied") + .HasColumnType("datetime(6)"); + + b.Property("ParentRequestId") + .HasColumnType("int"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("RequestedByAlias") + .HasColumnType("longtext"); + + b.Property("RequestedDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestedUserId") + .HasColumnType("varchar(255)"); + + b.Property("SeriesType") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Comment") + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("IssuesId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IssueCategoryId") + .HasColumnType("int"); + + b.Property("IssueId") + .HasColumnType("int"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("ResovledDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("UserReportedId") + .HasColumnType("varchar(255)"); + + 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() + .HasColumnType("int"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("Available") + .HasColumnType("tinyint(1)"); + + b.Property("Background") + .HasColumnType("longtext"); + + b.Property("Denied") + .HasColumnType("tinyint(1)"); + + b.Property("DeniedReason") + .HasColumnType("longtext"); + + b.Property("DigitalReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("IssueId") + .HasColumnType("int"); + + b.Property("LangCode") + .HasColumnType("longtext"); + + b.Property("MarkedAsApproved") + .HasColumnType("datetime(6)"); + + b.Property("MarkedAsAvailable") + .HasColumnType("datetime(6)"); + + b.Property("MarkedAsDenied") + .HasColumnType("datetime(6)"); + + b.Property("Overview") + .HasColumnType("longtext"); + + b.Property("PosterPath") + .HasColumnType("longtext"); + + b.Property("QualityOverride") + .HasColumnType("int"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("RequestedByAlias") + .HasColumnType("longtext"); + + b.Property("RequestedDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestedUserId") + .HasColumnType("varchar(255)"); + + b.Property("RootPathOverride") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeCount") + .HasColumnType("int"); + + b.Property("RequestDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Background") + .HasColumnType("longtext"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("Overview") + .HasColumnType("longtext"); + + b.Property("PosterPath") + .HasColumnType("longtext"); + + b.Property("QualityOverride") + .HasColumnType("int"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("RootFolder") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TotalSeasons") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Agent") + .HasColumnType("int"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("RadarrQualityProfile") + .HasColumnType("int"); + + b.Property("RadarrRootPath") + .HasColumnType("int"); + + b.Property("SonarrQualityProfile") + .HasColumnType("int"); + + b.Property("SonarrQualityProfileAnime") + .HasColumnType("int"); + + b.Property("SonarrRootPath") + .HasColumnType("int"); + + b.Property("SonarrRootPathAnime") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserQualityProfiles"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("RequestType") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("VoteType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Votes"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AirDate") + .HasColumnType("datetime(6)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("Available") + .HasColumnType("tinyint(1)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("Requested") + .HasColumnType("tinyint(1)"); + + b.Property("SeasonId") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ChildRequestId") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ombi.Store.Entities.MobileDevices", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("NotificationUserIds") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest") + .WithMany("ChildRequests") + .HasForeignKey("ParentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + + b.Navigation("ParentRequest"); + + b.Navigation("RequestedUser"); + }); + + 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"); + + b.Navigation("Issues"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory") + .WithMany() + .HasForeignKey("IssueCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", null) + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests", null) + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") + .WithMany() + .HasForeignKey("UserReportedId"); + + b.Navigation("IssueCategory"); + + b.Navigation("UserReported"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("UserNotificationPreferences") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest") + .WithMany("SeasonRequests") + .HasForeignKey("ChildRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildRequest"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Navigation("NotificationUserIds"); + + b.Navigation("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Navigation("Issues"); + + b.Navigation("SeasonRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Navigation("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Navigation("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Navigation("Episodes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiMySql/20210106132735_UserStreamingCountry.cs b/src/Ombi.Store/Migrations/OmbiMySql/20210106132735_UserStreamingCountry.cs new file mode 100644 index 000000000..1ee38e369 --- /dev/null +++ b/src/Ombi.Store/Migrations/OmbiMySql/20210106132735_UserStreamingCountry.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ombi.Store.Migrations.OmbiMySql +{ + public partial class UserStreamingCountry : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StreamingCountry", + table: "AspNetUsers", + type: "longtext", + nullable: false, + defaultValue: "US"); + + migrationBuilder.Sql("UPDATE AspNetUsers SET StreamingCountry = 'US'"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "StreamingCountry", + table: "AspNetUsers"); + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiMySql/OmbiMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiMySql/OmbiMySqlContextModelSnapshot.cs index 1678a77c1..38487dc9f 100644 --- a/src/Ombi.Store/Migrations/OmbiMySql/OmbiMySqlContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiMySql/OmbiMySqlContextModelSnapshot.cs @@ -14,8 +14,8 @@ namespace Ombi.Store.Migrations.OmbiMySql { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.1") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.1"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -27,18 +27,18 @@ namespace Ombi.Store.Migrations.OmbiMySql .HasColumnType("longtext"); b.Property("Name") - .HasColumnType("varchar(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("varchar(256)"); b.Property("NormalizedName") - .HasColumnType("varchar(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("varchar(256)"); b.HasKey("Id"); b.HasIndex("NormalizedName") .IsUnique() - .HasName("RoleNameIndex"); + .HasDatabaseName("RoleNameIndex"); b.ToTable("AspNetRoles"); }); @@ -257,8 +257,8 @@ namespace Ombi.Store.Migrations.OmbiMySql .HasColumnType("longtext"); b.Property("Email") - .HasColumnType("varchar(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("varchar(256)"); b.Property("EmailConfirmed") .HasColumnType("tinyint(1)"); @@ -285,12 +285,12 @@ namespace Ombi.Store.Migrations.OmbiMySql .HasColumnType("int"); b.Property("NormalizedEmail") - .HasColumnType("varchar(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("varchar(256)"); b.Property("NormalizedUserName") - .HasColumnType("varchar(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("varchar(256)"); b.Property("PasswordHash") .HasColumnType("longtext"); @@ -307,6 +307,9 @@ namespace Ombi.Store.Migrations.OmbiMySql b.Property("SecurityStamp") .HasColumnType("longtext"); + b.Property("StreamingCountry") + .HasColumnType("longtext"); + b.Property("TwoFactorEnabled") .HasColumnType("tinyint(1)"); @@ -314,8 +317,8 @@ namespace Ombi.Store.Migrations.OmbiMySql .HasColumnType("longtext"); b.Property("UserName") - .HasColumnType("varchar(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("varchar(256)"); b.Property("UserType") .HasColumnType("int"); @@ -323,11 +326,11 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasKey("Id"); b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); + .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") .IsUnique() - .HasName("UserNameIndex"); + .HasDatabaseName("UserNameIndex"); b.ToTable("AspNetUsers"); }); @@ -1017,6 +1020,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => @@ -1024,6 +1029,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany("NotificationUserIds") .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => @@ -1031,6 +1038,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => @@ -1038,6 +1047,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") .WithMany() .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => @@ -1051,6 +1062,10 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") .WithMany() .HasForeignKey("RequestedUserId"); + + b.Navigation("ParentRequest"); + + b.Navigation("RequestedUser"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => @@ -1062,6 +1077,10 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("Issues"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => @@ -1083,6 +1102,10 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") .WithMany() .HasForeignKey("UserReportedId"); + + b.Navigation("IssueCategory"); + + b.Navigation("UserReported"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => @@ -1090,6 +1113,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") .WithMany() .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => @@ -1097,6 +1122,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => @@ -1104,6 +1131,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => @@ -1111,6 +1140,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany("UserNotificationPreferences") .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => @@ -1118,6 +1149,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Votes", b => @@ -1125,6 +1158,8 @@ namespace Ombi.Store.Migrations.OmbiMySql b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => @@ -1134,6 +1169,8 @@ namespace Ombi.Store.Migrations.OmbiMySql .HasForeignKey("SeasonId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Season"); }); modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => @@ -1143,6 +1180,42 @@ namespace Ombi.Store.Migrations.OmbiMySql .HasForeignKey("ChildRequestId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ChildRequest"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Navigation("NotificationUserIds"); + + b.Navigation("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Navigation("Issues"); + + b.Navigation("SeasonRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Navigation("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Navigation("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Navigation("Episodes"); }); #pragma warning restore 612, 618 } diff --git a/src/Ombi.Store/Migrations/OmbiSqlite/20210106134000_UserStreamingCountry.Designer.cs b/src/Ombi.Store/Migrations/OmbiSqlite/20210106134000_UserStreamingCountry.Designer.cs new file mode 100644 index 000000000..e9c6e1f20 --- /dev/null +++ b/src/Ombi.Store/Migrations/OmbiSqlite/20210106134000_UserStreamingCountry.Designer.cs @@ -0,0 +1,1225 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.Sqlite; + +namespace Ombi.Store.Migrations.OmbiSqlite +{ + [DbContext(typeof(OmbiSqliteContext))] + [Migration("20210106134000_UserStreamingCountry")] + partial class UserStreamingCountry + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuditArea") + .HasColumnType("INTEGER"); + + b.Property("AuditType") + .HasColumnType("INTEGER"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("User") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.MobileDevices", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Token") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("MobileDevices"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Agent") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("NotificationType") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("EpisodeRequestLimit") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastLoggedIn") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MovieRequestLimit") + .HasColumnType("INTEGER"); + + b.Property("MusicRequestLimit") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("StreamingCountry") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserAccessToken") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("AlbumId") + .HasColumnType("TEXT"); + + b.Property("ContentId") + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestQueue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("Dts") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RequestQueue"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestSubscription"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("ArtistName") + .HasColumnType("TEXT"); + + b.Property("Available") + .HasColumnType("INTEGER"); + + b.Property("Cover") + .HasColumnType("TEXT"); + + b.Property("Denied") + .HasColumnType("INTEGER"); + + b.Property("DeniedReason") + .HasColumnType("TEXT"); + + b.Property("Disk") + .HasColumnType("TEXT"); + + b.Property("ForeignAlbumId") + .HasColumnType("TEXT"); + + b.Property("ForeignArtistId") + .HasColumnType("TEXT"); + + b.Property("MarkedAsApproved") + .HasColumnType("TEXT"); + + b.Property("MarkedAsAvailable") + .HasColumnType("TEXT"); + + b.Property("MarkedAsDenied") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("RequestedByAlias") + .HasColumnType("TEXT"); + + b.Property("RequestedDate") + .HasColumnType("TEXT"); + + b.Property("RequestedUserId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("AlbumRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("Available") + .HasColumnType("INTEGER"); + + b.Property("Denied") + .HasColumnType("INTEGER"); + + b.Property("DeniedReason") + .HasColumnType("TEXT"); + + b.Property("IssueId") + .HasColumnType("INTEGER"); + + b.Property("MarkedAsApproved") + .HasColumnType("TEXT"); + + b.Property("MarkedAsAvailable") + .HasColumnType("TEXT"); + + b.Property("MarkedAsDenied") + .HasColumnType("TEXT"); + + b.Property("ParentRequestId") + .HasColumnType("INTEGER"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("RequestedByAlias") + .HasColumnType("TEXT"); + + b.Property("RequestedDate") + .HasColumnType("TEXT"); + + b.Property("RequestedUserId") + .HasColumnType("TEXT"); + + b.Property("SeriesType") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("IssuesId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IssueCategoryId") + .HasColumnType("INTEGER"); + + b.Property("IssueId") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("ResovledDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("UserReportedId") + .HasColumnType("TEXT"); + + 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() + .HasColumnType("INTEGER"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("Available") + .HasColumnType("INTEGER"); + + b.Property("Background") + .HasColumnType("TEXT"); + + b.Property("Denied") + .HasColumnType("INTEGER"); + + b.Property("DeniedReason") + .HasColumnType("TEXT"); + + b.Property("DigitalReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("IssueId") + .HasColumnType("INTEGER"); + + b.Property("LangCode") + .HasColumnType("TEXT"); + + b.Property("MarkedAsApproved") + .HasColumnType("TEXT"); + + b.Property("MarkedAsAvailable") + .HasColumnType("TEXT"); + + b.Property("MarkedAsDenied") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("PosterPath") + .HasColumnType("TEXT"); + + b.Property("QualityOverride") + .HasColumnType("INTEGER"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("RequestedByAlias") + .HasColumnType("TEXT"); + + b.Property("RequestedDate") + .HasColumnType("TEXT"); + + b.Property("RequestedUserId") + .HasColumnType("TEXT"); + + b.Property("RootPathOverride") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeCount") + .HasColumnType("INTEGER"); + + b.Property("RequestDate") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Background") + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("PosterPath") + .HasColumnType("TEXT"); + + b.Property("QualityOverride") + .HasColumnType("INTEGER"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("RootFolder") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSeasons") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Token") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Agent") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RadarrQualityProfile") + .HasColumnType("INTEGER"); + + b.Property("RadarrRootPath") + .HasColumnType("INTEGER"); + + b.Property("SonarrQualityProfile") + .HasColumnType("INTEGER"); + + b.Property("SonarrQualityProfileAnime") + .HasColumnType("INTEGER"); + + b.Property("SonarrRootPath") + .HasColumnType("INTEGER"); + + b.Property("SonarrRootPathAnime") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserQualityProfiles"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("RequestType") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VoteType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Votes"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AirDate") + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("Available") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Requested") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChildRequestId") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ombi.Store.Entities.MobileDevices", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("NotificationUserIds") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest") + .WithMany("ChildRequests") + .HasForeignKey("ParentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + + b.Navigation("ParentRequest"); + + b.Navigation("RequestedUser"); + }); + + 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"); + + b.Navigation("Issues"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory") + .WithMany() + .HasForeignKey("IssueCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", null) + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests", null) + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") + .WithMany() + .HasForeignKey("UserReportedId"); + + b.Navigation("IssueCategory"); + + b.Navigation("UserReported"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("UserNotificationPreferences") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Votes", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest") + .WithMany("SeasonRequests") + .HasForeignKey("ChildRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildRequest"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Navigation("NotificationUserIds"); + + b.Navigation("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Navigation("Issues"); + + b.Navigation("SeasonRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Navigation("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Navigation("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Navigation("Episodes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiSqlite/20210106134000_UserStreamingCountry.cs b/src/Ombi.Store/Migrations/OmbiSqlite/20210106134000_UserStreamingCountry.cs new file mode 100644 index 000000000..3e5ff7651 --- /dev/null +++ b/src/Ombi.Store/Migrations/OmbiSqlite/20210106134000_UserStreamingCountry.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ombi.Store.Migrations.OmbiSqlite +{ + public partial class UserStreamingCountry : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StreamingCountry", + table: "AspNetUsers", + type: "TEXT", + nullable: false, + defaultValue: "US"); + + migrationBuilder.Sql("UPDATE AspNetUsers SET StreamingCountry = 'US'"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "StreamingCountry", + table: "AspNetUsers"); + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiSqlite/OmbiSqliteContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiSqlite/OmbiSqliteContextModelSnapshot.cs index 7e9457be4..6a00fe29b 100644 --- a/src/Ombi.Store/Migrations/OmbiSqlite/OmbiSqliteContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiSqlite/OmbiSqliteContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Ombi.Store.Migrations.OmbiSqlite { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.1"); + .HasAnnotation("ProductVersion", "5.0.1"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -26,18 +26,18 @@ namespace Ombi.Store.Migrations.OmbiSqlite .HasColumnType("TEXT"); b.Property("Name") - .HasColumnType("TEXT") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("TEXT"); b.Property("NormalizedName") - .HasColumnType("TEXT") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("TEXT"); b.HasKey("Id"); b.HasIndex("NormalizedName") .IsUnique() - .HasName("RoleNameIndex"); + .HasDatabaseName("RoleNameIndex"); b.ToTable("AspNetRoles"); }); @@ -256,8 +256,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite .HasColumnType("TEXT"); b.Property("Email") - .HasColumnType("TEXT") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("TEXT"); b.Property("EmailConfirmed") .HasColumnType("INTEGER"); @@ -284,12 +284,12 @@ namespace Ombi.Store.Migrations.OmbiSqlite .HasColumnType("INTEGER"); b.Property("NormalizedEmail") - .HasColumnType("TEXT") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("TEXT"); b.Property("NormalizedUserName") - .HasColumnType("TEXT") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("TEXT"); b.Property("PasswordHash") .HasColumnType("TEXT"); @@ -306,6 +306,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.Property("SecurityStamp") .HasColumnType("TEXT"); + b.Property("StreamingCountry") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("TwoFactorEnabled") .HasColumnType("INTEGER"); @@ -313,8 +317,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite .HasColumnType("TEXT"); b.Property("UserName") - .HasColumnType("TEXT") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("TEXT"); b.Property("UserType") .HasColumnType("INTEGER"); @@ -322,11 +326,11 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasKey("Id"); b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); + .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") .IsUnique() - .HasName("UserNameIndex"); + .HasDatabaseName("UserNameIndex"); b.ToTable("AspNetUsers"); }); @@ -1016,6 +1020,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => @@ -1023,6 +1029,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany("NotificationUserIds") .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => @@ -1030,6 +1038,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => @@ -1037,6 +1047,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") .WithMany() .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => @@ -1050,6 +1062,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") .WithMany() .HasForeignKey("RequestedUserId"); + + b.Navigation("ParentRequest"); + + b.Navigation("RequestedUser"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => @@ -1061,6 +1077,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("Issues"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => @@ -1082,6 +1102,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") .WithMany() .HasForeignKey("UserReportedId"); + + b.Navigation("IssueCategory"); + + b.Navigation("UserReported"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => @@ -1089,6 +1113,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") .WithMany() .HasForeignKey("RequestedUserId"); + + b.Navigation("RequestedUser"); }); modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => @@ -1096,6 +1122,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => @@ -1103,6 +1131,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => @@ -1110,6 +1140,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany("UserNotificationPreferences") .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => @@ -1117,6 +1149,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Entities.Votes", b => @@ -1124,6 +1158,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite b.HasOne("Ombi.Store.Entities.OmbiUser", "User") .WithMany() .HasForeignKey("UserId"); + + b.Navigation("User"); }); modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => @@ -1133,6 +1169,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite .HasForeignKey("SeasonId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Season"); }); modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => @@ -1142,6 +1180,42 @@ namespace Ombi.Store.Migrations.OmbiSqlite .HasForeignKey("ChildRequestId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ChildRequest"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Navigation("NotificationUserIds"); + + b.Navigation("UserNotificationPreferences"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Navigation("Issues"); + + b.Navigation("SeasonRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Navigation("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Navigation("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Navigation("Episodes"); }); #pragma warning restore 612, 618 } diff --git a/src/Ombi.Store/Repository/AuditRepository.cs b/src/Ombi.Store/Repository/AuditRepository.cs index 2892e3e84..453659f85 100644 --- a/src/Ombi.Store/Repository/AuditRepository.cs +++ b/src/Ombi.Store/Repository/AuditRepository.cs @@ -24,20 +24,16 @@ namespace Ombi.Store.Repository public async Task Record(AuditType type, AuditArea area, string description, string user) { - using (var tran = await Ctx.Database.BeginTransactionAsync()) + await Ctx.Audit.AddAsync(new Audit { - await Ctx.Audit.AddAsync(new Audit - { - User = user, - AuditArea = area, - AuditType = type, - DateTime = DateTime.UtcNow, - Description = description - }); + User = user, + AuditArea = area, + AuditType = type, + DateTime = DateTime.UtcNow, + Description = description + }); - await Ctx.SaveChangesAsync(); - tran.Commit(); - } + await Ctx.SaveChangesAsync(); } } } diff --git a/src/Ombi.Store/Repository/IJellyfinContentRepository.cs b/src/Ombi.Store/Repository/IJellyfinContentRepository.cs new file mode 100644 index 000000000..ff1f2d7dc --- /dev/null +++ b/src/Ombi.Store/Repository/IJellyfinContentRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public interface IJellyfinContentRepository : IRepository + { + IQueryable Get(); + Task GetByTheMovieDbId(string mov); + Task GetByTvDbId(string tv); + Task GetByImdbId(string imdbid); + Task GetByJellyfinId(string jellyfinId); + Task Update(JellyfinContent existingContent); + IQueryable GetAllEpisodes(); + Task Add(JellyfinEpisode content); + Task GetEpisodeByJellyfinId(string key); + Task AddRange(IEnumerable content); + + void UpdateWithoutSave(JellyfinContent existingContent); + } +} diff --git a/src/Ombi.Store/Repository/JellyfinContentRepository.cs b/src/Ombi.Store/Repository/JellyfinContentRepository.cs new file mode 100644 index 000000000..2b84adb00 --- /dev/null +++ b/src/Ombi.Store/Repository/JellyfinContentRepository.cs @@ -0,0 +1,106 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: PlexContentRepository.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Ombi.Store.Context; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public class JellyfinContentRepository : ExternalRepository, IJellyfinContentRepository + { + + public JellyfinContentRepository(ExternalContext db):base(db) + { + Db = db; + } + + private ExternalContext Db { get; } + + + public async Task GetByImdbId(string imdbid) + { + return await Db.JellyfinContent.FirstOrDefaultAsync(x => x.ImdbId == imdbid); + } + public async Task GetByTvDbId(string tv) + { + return await Db.JellyfinContent.FirstOrDefaultAsync(x => x.TvDbId == tv); + } + public async Task GetByTheMovieDbId(string mov) + { + return await Db.JellyfinContent.FirstOrDefaultAsync(x => x.TheMovieDbId == mov); + } + + public IQueryable Get() + { + return Db.JellyfinContent.AsQueryable(); + } + + public async Task GetByJellyfinId(string jellyfinId) + { + return await Db.JellyfinContent./*Include(x => x.Seasons).*/FirstOrDefaultAsync(x => x.JellyfinId == jellyfinId); + } + + public async Task Update(JellyfinContent existingContent) + { + Db.JellyfinContent.Update(existingContent); + await InternalSaveChanges(); + } + + public IQueryable GetAllEpisodes() + { + return Db.JellyfinEpisode.AsQueryable(); + } + + public async Task Add(JellyfinEpisode content) + { + await Db.JellyfinEpisode.AddAsync(content); + await InternalSaveChanges(); + return content; + } + public async Task GetEpisodeByJellyfinId(string key) + { + return await Db.JellyfinEpisode.FirstOrDefaultAsync(x => x.JellyfinId == key); + } + + public async Task AddRange(IEnumerable content) + { + Db.JellyfinEpisode.AddRange(content); + await InternalSaveChanges(); + } + + public void UpdateWithoutSave(JellyfinContent existingContent) + { + Db.JellyfinContent.Update(existingContent); + } + + } +} diff --git a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs index 985f464fd..a5a7406ab 100644 --- a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs @@ -22,21 +22,21 @@ namespace Ombi.Store.Repository.Requests { return await Db.TvRequests.Where(x => x.TvDbId == tvDbId) .Include(x => x.ChildRequests) - .ThenInclude(x => x.RequestedUser) + .ThenInclude(x => x.RequestedUser) .Include(x => x.ChildRequests) - .ThenInclude(x => x.SeasonRequests) - .ThenInclude(x => x.Episodes) + .ThenInclude(x => x.SeasonRequests) + .ThenInclude(x => x.Episodes) .FirstOrDefaultAsync(); } public TvRequests GetRequest(int tvDbId) { - return Db.TvRequests.Where(x => x.TvDbId == tvDbId) + return Db.TvRequests.Where(x => x.TvDbId == tvDbId).AsSplitQuery() .Include(x => x.ChildRequests) - .ThenInclude(x => x.RequestedUser) + .ThenInclude(x => x.RequestedUser) .Include(x => x.ChildRequests) - .ThenInclude(x => x.SeasonRequests) - .ThenInclude(x => x.Episodes) + .ThenInclude(x => x.SeasonRequests) + .ThenInclude(x => x.Episodes) .FirstOrDefault(); } diff --git a/src/Ombi.Store/Repository/SettingsJsonRepository.cs b/src/Ombi.Store/Repository/SettingsJsonRepository.cs index 8fd3d46da..0fd71298a 100644 --- a/src/Ombi.Store/Repository/SettingsJsonRepository.cs +++ b/src/Ombi.Store/Repository/SettingsJsonRepository.cs @@ -25,27 +25,21 @@ namespace Ombi.Store.Repository { //_cache.Remove(GetName(entity.SettingsName)); - using (var tran = Db.Database.BeginTransaction()) - { - var settings = Db.Settings.Add(entity); - Db.SaveChanges(); - tran.Commit(); - return settings.Entity; - } + var settings = Db.Settings.Add(entity); + Db.SaveChanges(); + return settings.Entity; + } public async Task InsertAsync(GlobalSettings entity) { - using (var tran = Db.Database.BeginTransaction()) - { - //_cache.Remove(GetName(entity.SettingsName)); - var settings = await Db.Settings.AddAsync(entity); - await Db.SaveChangesAsync(); - tran.Commit(); + //_cache.Remove(GetName(entity.SettingsName)); + var settings = await Db.Settings.AddAsync(entity); + await Db.SaveChangesAsync(); + + return settings.Entity; - return settings.Entity; - } } @@ -87,23 +81,15 @@ namespace Ombi.Store.Repository { //_cache.Remove(GetName(entity.SettingsName)); - using (var tran = Db.Database.BeginTransaction()) - { - Db.Settings.Remove(entity); - Db.SaveChanges(); - tran.Commit(); - } + Db.Settings.Remove(entity); + Db.SaveChanges(); } public void Update(GlobalSettings entity) { - using (var tran = Db.Database.BeginTransaction()) - { - Db.Update(entity); - //_cache.Remove(GetName(entity.SettingsName)); - Db.SaveChanges(); - tran.Commit(); - } + Db.Update(entity); + //_cache.Remove(GetName(entity.SettingsName)); + Db.SaveChanges(); } private string GetName(string entity) @@ -113,13 +99,8 @@ namespace Ombi.Store.Repository private async Task InternalSaveChanges() { - - using (var tran = Db.Database.BeginTransaction()) - { - var r = await Db.SaveChangesAsync(); - tran.Commit(); - return r; - } + var r = await Db.SaveChangesAsync(); + return r; } } } \ No newline at end of file diff --git a/src/Ombi.Test.Common/MockHelper.cs b/src/Ombi.Test.Common/MockHelper.cs index 00e327abc..8d3f5ce8d 100644 --- a/src/Ombi.Test.Common/MockHelper.cs +++ b/src/Ombi.Test.Common/MockHelper.cs @@ -14,7 +14,7 @@ namespace Ombi.Test.Common { 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); + var mgr = new Mock(store.Object, null, null, null, null, null, null, null, null, null, null, null, null, null, null); mgr.Object.UserValidators.Add(new UserValidator()); mgr.Object.PasswordValidators.Add(new PasswordValidator()); diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index c9e21c2ec..1b35381ad 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -13,7 +13,7 @@ namespace Ombi.Api.TheMovieDb Task> NowPlaying(string languageCode, int? page = null); Task> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task> SearchMovie(string searchTerm, int? year, string languageCode); - Task> SearchTv(string searchTerm); + Task> SearchTv(string searchTerm, string year = default); Task> TopRated(string languageCode, int? page = null); Task> Upcoming(string languageCode, int? page = null); Task> SimilarMovies(int movieId, string langCode); @@ -28,5 +28,7 @@ namespace Ombi.Api.TheMovieDb Task GetCollection(string langCode, int collectionId, CancellationToken cancellationToken); Task> SearchKeyword(string searchTerm); Task GetKeyword(int keywordId); + Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token); + Task GetTvWatchProviders(int theMoviedbId, CancellationToken token); } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/WatchProviders.cs b/src/Ombi.TheMovieDbApi/Models/WatchProviders.cs new file mode 100644 index 000000000..bcd4c2418 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/WatchProviders.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Ombi.Api.TheMovieDb.Models +{ + public class WatchProviders + { + [JsonProperty("id")] + public int Id { get; set; } + [JsonProperty("results")] + public Results Results { get; set; } + } + + public class Results + { + public WatchProviderData AR { get; set; } + public WatchProviderData AT { get; set; } + public WatchProviderData AU { get; set; } + public WatchProviderData BE { get; set; } + public WatchProviderData BR { get; set; } + public WatchProviderData CA { get; set; } + public WatchProviderData CH { get; set; } + public WatchProviderData CL { get; set; } + public WatchProviderData CO { get; set; } + public WatchProviderData CZ { get; set; } + public WatchProviderData DE { get; set; } + public WatchProviderData DK { get; set; } + public WatchProviderData EC { get; set; } + public WatchProviderData EE { get; set; } + public WatchProviderData ES { get; set; } + public WatchProviderData FI { get; set; } + public WatchProviderData FR { get; set; } + public WatchProviderData GB { get; set; } + public WatchProviderData GR { get; set; } + public WatchProviderData HU { get; set; } + public WatchProviderData ID { get; set; } + public WatchProviderData IE { get; set; } + public WatchProviderData IN { get; set; } + public WatchProviderData IT { get; set; } + public WatchProviderData JP { get; set; } + public WatchProviderData KR { get; set; } + public WatchProviderData LT { get; set; } + public WatchProviderData LV { get; set; } + public WatchProviderData MX { get; set; } + public WatchProviderData MY { get; set; } + public WatchProviderData NL { get; set; } + public WatchProviderData NO { get; set; } + public WatchProviderData NZ { get; set; } + public WatchProviderData PE { get; set; } + public WatchProviderData PH { get; set; } + public WatchProviderData PL { get; set; } + public WatchProviderData PT { get; set; } + public WatchProviderData RU { get; set; } + public WatchProviderData SE { get; set; } + public WatchProviderData SG { get; set; } + public WatchProviderData TH { get; set; } + public WatchProviderData TR { get; set; } + public WatchProviderData US { get; set; } + public WatchProviderData VE { get; set; } + public WatchProviderData ZA { get; set; } + } + + public class WatchProviderData + { + public string link { get; set; } + [JsonProperty("flatrate")] + public List StreamInformation { get; set; } + } + + public class StreamData + { + public int display_priority { get; set; } + public string logo_path { get; set; } + public int provider_id { get; set; } + public string provider_name { get; set; } + } +} diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index c9deede55..f270ad3b2 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -10,6 +10,7 @@ using Nito.AsyncEx; using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; using Ombi.TheMovieDbApi.Models; namespace Ombi.Api.TheMovieDb @@ -24,7 +25,7 @@ namespace Ombi.Api.TheMovieDb } private const string ApiToken = "b8eabaf5608b88d0298aa189dd90bf00"; - private const string BaseUri ="http://api.themoviedb.org/3/"; + private const string BaseUri = "http://api.themoviedb.org/3/"; private IMapper Mapper { get; } private IApi Api { get; } private AsyncLazy Settings { get; } @@ -107,11 +108,15 @@ namespace Ombi.Api.TheMovieDb return result; } - public async Task> SearchTv(string searchTerm) + public async Task> SearchTv(string searchTerm, string year = default) { var request = new Request($"search/tv", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); + if (year.HasValue()) + { + request.AddQueryString("first_air_date_year", year); + } AddRetry(request); var result = await Api.Request>(request); @@ -126,7 +131,7 @@ namespace Ombi.Api.TheMovieDb return await Api.Request(request); } - + public async Task> SimilarMovies(int movieId, string langCode) { var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); @@ -165,7 +170,7 @@ namespace Ombi.Api.TheMovieDb AddRetry(request); - var result = await Api.Request>(request); + var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -299,6 +304,32 @@ namespace Ombi.Api.TheMovieDb return keyword == null || keyword.Id == 0 ? null : keyword; } + public Task> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken) + { + var request = new Request("search/multi", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", languageCode); + request.AddQueryString("query", searchTerm); + var result = Api.Request>(request, cancellationToken); + return result; + } + + public Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token) + { + var request = new Request($"movie/{theMoviedbId}/watch/providers", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + + return Api.Request(request, token); + } + + public Task GetTvWatchProviders(int theMoviedbId, CancellationToken token) + { + var request = new Request($"tv/{theMoviedbId}/watch/providers", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + + return Api.Request(request, token); + } + private async Task AddDiscoverMovieSettings(Request request) { var settings = await Settings; @@ -309,17 +340,6 @@ namespace Ombi.Api.TheMovieDb } } - - public async Task> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken) - { - var request = new Request("search/multi", BaseUri, HttpMethod.Get); - request.AddQueryString("api_key", ApiToken); - request.AddQueryString("language", languageCode); - request.AddQueryString("query", searchTerm); - var result = await Api.Request>(request, cancellationToken); - return result; - } - private static void AddRetry(Request request) { request.Retry = true; diff --git a/src/Ombi.sln b/src/Ombi.sln index 7a32a4bf9..70324f967 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Schedule", "Ombi.Sched EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Emby", "Ombi.Api.Emby\Ombi.Api.Emby.csproj", "{08FF107D-31E1-470D-AF86-E09B015CEE06}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Jellyfin", "Ombi.Api.Jellyfin\Ombi.Api.Jellyfin.csproj", "{F03757C7-5145-45C9-AFFF-B4E946755779}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Sonarr", "Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj", "{CFB5E008-D0D0-43C0-AA06-89E49D17F384}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6F42AB98-9196-44C4-B888-D5E409F415A1}" @@ -119,6 +121,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Webhook", "Ombi.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.CloudService", "Ombi.Api.CloudService\Ombi.Api.CloudService.csproj", "{5DE40A66-B369-469E-8626-ECE23D9D8034}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.RottenTomatoes", "Ombi.Api.RottenTomatoes\Ombi.Api.RottenTomatoes.csproj", "{8F19C701-7881-4BC7-8BBA-B068A6B954AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -169,6 +173,10 @@ Global {08FF107D-31E1-470D-AF86-E09B015CEE06}.Debug|Any CPU.Build.0 = Debug|Any CPU {08FF107D-31E1-470D-AF86-E09B015CEE06}.Release|Any CPU.ActiveCfg = Release|Any CPU {08FF107D-31E1-470D-AF86-E09B015CEE06}.Release|Any CPU.Build.0 = Release|Any CPU + {F03757C7-5145-45C9-AFFF-B4E946755779}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F03757C7-5145-45C9-AFFF-B4E946755779}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F03757C7-5145-45C9-AFFF-B4E946755779}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F03757C7-5145-45C9-AFFF-B4E946755779}.Release|Any CPU.Build.0 = Release|Any CPU {CFB5E008-D0D0-43C0-AA06-89E49D17F384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CFB5E008-D0D0-43C0-AA06-89E49D17F384}.Debug|Any CPU.Build.0 = Debug|Any CPU {CFB5E008-D0D0-43C0-AA06-89E49D17F384}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -317,6 +325,10 @@ Global {5DE40A66-B369-469E-8626-ECE23D9D8034}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DE40A66-B369-469E-8626-ECE23D9D8034}.Release|Any CPU.ActiveCfg = Release|Any CPU {5DE40A66-B369-469E-8626-ECE23D9D8034}.Release|Any CPU.Build.0 = Release|Any CPU + {8F19C701-7881-4BC7-8BBA-B068A6B954AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F19C701-7881-4BC7-8BBA-B068A6B954AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F19C701-7881-4BC7-8BBA-B068A6B954AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F19C701-7881-4BC7-8BBA-B068A6B954AD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -328,6 +340,7 @@ Global {63E63511-1C7F-4162-8F92-8F7391B3C8A3} = {025FB189-2FFB-4F43-A64B-6F1B5A0D2065} {2E1A7B91-F29B-42BC-8F1E-1CF2DCC389BA} = {9293CA11-360A-4C20-A674-B9E794431BF5} {08FF107D-31E1-470D-AF86-E09B015CEE06} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {F03757C7-5145-45C9-AFFF-B4E946755779} = {9293CA11-360A-4C20-A674-B9E794431BF5} {CFB5E008-D0D0-43C0-AA06-89E49D17F384} = {9293CA11-360A-4C20-A674-B9E794431BF5} {0E8EF835-E4F0-4EE5-A2B6-678DEE973721} = {9293CA11-360A-4C20-A674-B9E794431BF5} {E6EE2830-E4AC-4F2E-AD93-2C9305605761} = {EA30DD15-6280-4687-B370-2956EC2E54E5} @@ -363,6 +376,7 @@ Global {59D19538-0496-44EE-936E-EBBC22CF7B27} = {410F36CF-9C60-428A-B191-6FD90610991A} {E2186FDA-D827-4781-8663-130AC382F12C} = {9293CA11-360A-4C20-A674-B9E794431BF5} {5DE40A66-B369-469E-8626-ECE23D9D8034} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {8F19C701-7881-4BC7-8BBA-B068A6B954AD} = {9293CA11-360A-4C20-A674-B9E794431BF5} 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 0a45aaea0..642e584c5 100644 --- a/src/Ombi/.vscode/launch.json +++ b/src/Ombi/.vscode/launch.json @@ -1,6 +1,29 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Launch Backend", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net5.0/ombi.dll", + "args": ["--host", "http://localhost:3577"], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole" + }, + { + "name": "Launch Frontend", + "type": "pwa-node", + "request": "launch", + "runtimeExecutable": "yarn", + "cwd": "${workspaceFolder}/ClientApp", + "runtimeArgs": ["start"], + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!**/node_modules/**" + ], + }, { "name": "ng serve", "type": "chrome", diff --git a/src/Ombi/Attributes/WizardActionFilter.cs b/src/Ombi/Attributes/WizardActionFilter.cs new file mode 100644 index 000000000..644256d08 --- /dev/null +++ b/src/Ombi/Attributes/WizardActionFilter.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Ombi.Core.Settings; +using Ombi.Settings.Settings.Models; +using System.Threading.Tasks; + +namespace Ombi.Attributes +{ + public class WizardActionFilter : IAsyncActionFilter + { + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var settingsService = context.HttpContext.RequestServices.GetRequiredService>(); + + var settings = await settingsService.GetSettingsAsync(); + + if (!settings.Wizard) + { + await next(); + return; + } + context.Result = new UnauthorizedResult(); + } + } +} diff --git a/src/Ombi/ClientApp/angular.json b/src/Ombi/ClientApp/angular.json index 4038c9a56..2e3ff44e5 100644 --- a/src/Ombi/ClientApp/angular.json +++ b/src/Ombi/ClientApp/angular.json @@ -25,11 +25,14 @@ "src/assets" ], "styles": [ - "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles/_imports.scss", "node_modules/bootstrap/scss/bootstrap.scss", "node_modules/primeng/resources/themes/md-dark-deeppurple/theme.css", - "node_modules/font-awesome/scss/font-awesome.scss", + "node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss", + "node_modules/@fortawesome/fontawesome-free/scss/regular.scss", + "node_modules/@fortawesome/fontawesome-free/scss/solid.scss", + "node_modules/@fortawesome/fontawesome-free/scss/brands.scss", "node_modules/primeng/resources/primeng.min.css", "node_modules/primeicons/primeicons.css", "node_modules/please-wait/src/please-wait.scss", diff --git a/src/Ombi/ClientApp/package.json b/src/Ombi/ClientApp/package.json index 988d20daf..6c1713eff 100644 --- a/src/Ombi/ClientApp/package.json +++ b/src/Ombi/ClientApp/package.json @@ -24,6 +24,7 @@ "@angularclass/hmr": "^2.1.3", "@aspnet/signalr": "^1.1.0", "@auth0/angular-jwt": "^2.1.0", + "@fortawesome/fontawesome-free": "^5.15.2", "@fullcalendar/core": "^4.2.0", "@fullcalendar/daygrid": "^4.4.0", "@fullcalendar/interaction": "^4.2.0", @@ -39,9 +40,9 @@ "chart.js": "2.9.4", "core-js": "^2.5.4", "eventemitter2": "^5.0.1", - "font-awesome": "^4.7.0", "fullcalendar": "^4.0.0-alpha.4", "jquery": "3.3.1", + "lodash": "^4.17.20", "moment": "^2.23.0", "ng2-cookies": "^1.0.12", "ngx-clipboard": "^12.1.0", @@ -51,8 +52,9 @@ "please-wait": "^0.0.5", "popper.js": "^1.14.3", "primeicons": "^4.0.0", - "primeng": "^10.0.3", + "primeng": "^11.0.0", "rxjs": "^6.5.2", + "sass-recursive-map-merge": "^1.0.1", "spinkit": "^1.2.5", "store": "^2.0.12", "ts-md5": "^1.2.7", diff --git a/src/Ombi/ClientApp/src/app/app.component.html b/src/Ombi/ClientApp/src/app/app.component.html index d4297aeb4..a8e527a6b 100644 --- a/src/Ombi/ClientApp/src/app/app.component.html +++ b/src/Ombi/ClientApp/src/app/app.component.html @@ -45,7 +45,7 @@ @@ -170,7 +170,7 @@
- + diff --git a/src/Ombi/ClientApp/src/app/app.component.ts b/src/Ombi/ClientApp/src/app/app.component.ts index f6f9fe62a..40ece5d31 100644 --- a/src/Ombi/ClientApp/src/app/app.component.ts +++ b/src/Ombi/ClientApp/src/app/app.component.ts @@ -85,8 +85,6 @@ export class AppComponent implements OnInit { public ngOnInit() { window["loading_screen"].finish(); - const theme = this.storage.get("theme"); - this.onSetTheme(theme); this.settingsService.getCustomization().subscribe(x => { this.customizationSettings = x; @@ -131,11 +129,4 @@ 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 09e5f2955..cfaf783cf 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -1,5 +1,5 @@ import { CommonModule, PlatformLocation, APP_BASE_HREF } from "@angular/common"; -import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; @@ -66,6 +66,8 @@ import { StorageService } from "./shared/storage/storage-service"; import { SignalRNotificationService } from "./services/signlarnotification.service"; import { MatMenuModule } from "@angular/material/menu"; import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component"; +import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor"; +import { FilterService } from "./discover/services/filter-service"; const routes: Routes = [ { path: "*", component: PageNotFoundComponent }, @@ -192,10 +194,16 @@ export function JwtTokenGetter() { MessageService, StorageService, RequestService, + FilterService, SignalRNotificationService, { provide: APP_BASE_HREF, useValue: window["baseHref"] + }, + { + provide: HTTP_INTERCEPTORS, + useClass: UnauthorizedInterceptor, + multi: true } ], bootstrap: [AppComponent], diff --git a/src/Ombi/ClientApp/src/app/auth/unauthorized.interceptor.ts b/src/Ombi/ClientApp/src/app/auth/unauthorized.interceptor.ts new file mode 100644 index 000000000..c70b649be --- /dev/null +++ b/src/Ombi/ClientApp/src/app/auth/unauthorized.interceptor.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; +import { Observable, Subject, throwError } from 'rxjs'; +import { catchError, throttleTime } from 'rxjs/operators'; +import { AuthService } from './auth.service'; +import { Router } from '@angular/router'; + +@Injectable() +export class UnauthorizedInterceptor implements HttpInterceptor { + + private throttleLogout = new Subject(); + constructor(private authService: AuthService, private router: Router) { + this.throttleLogout.pipe(throttleTime(5000)).subscribe(url => { + this.authService.logout(); + this.router.navigate(["login"]); + }); + } + + public intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + catchError((response: HttpErrorResponse) => { + if (response.status === 401) { + this.throttleLogout.next(); + } + return throwError(response); + } + )); + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/custompage/custompage.component.html b/src/Ombi/ClientApp/src/app/custompage/custompage.component.html index d35c94141..ae2d00eb7 100644 --- a/src/Ombi/ClientApp/src/app/custompage/custompage.component.html +++ b/src/Ombi/ClientApp/src/app/custompage/custompage.component.html @@ -5,7 +5,7 @@
@@ -14,7 +14,7 @@
diff --git a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.html b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.html index 877d799d0..3ac12d570 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.html @@ -16,27 +16,35 @@ + class="fas fa-play-circle fa-2x grow"> + class="fas fa-play-circle fa-2x grow"> + + + + class="fas fa-play-circle fa-2x grow"> + class="fas fa-play-circle fa-2x grow"> + + + - +
@@ -115,14 +123,14 @@ - @@ -132,24 +140,27 @@ + {{'Common.Available' | translate }} + {{'Common.PartiallyAvailable' | translate }} {{'Search.ViewOnPlex' | + href="{{tv.plexUrl}}" target="_blank"> {{'Search.ViewOnPlex' | translate}} {{'Search.ViewOnEmby' | + target="_blank"> {{'Search.ViewOnEmby' | + translate}} + {{'Search.ViewOnJellyfin' | translate}}
- \ No newline at end of file + diff --git a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.scss b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.scss index 27079acbf..e7fe00c60 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.scss +++ b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.scss @@ -42,4 +42,4 @@ h3 strong { .overview { height:300px; overflow-y: auto; -} +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.ts b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.ts index fa13b1866..804e0e9ec 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card-details.component.ts @@ -31,9 +31,9 @@ export class DiscoverCardDetailsComponent implements OnInit { public async ngOnInit() { this.loading = true; if (this.data.type === RequestType.movie) { - this.movie = await this.searchService.getFullMovieDetailsPromise(this.data.id); + 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); + this.tv = await this.searchService.getTvInfo(+this.data.id); const creator = this.tv.crew.filter(tv => { return tv.type === "Creator"; })[0]; @@ -67,7 +67,7 @@ export class DiscoverCardDetailsComponent implements OnInit { public async request() { this.loading = true; if (this.data.type === RequestType.movie) { - const result = await this.requestService.requestMovie({ theMovieDbId: this.data.id, languageCode: "" }).toPromise(); + const result = await this.requestService.requestMovie({ theMovieDbId: +this.data.id, languageCode: "", requestOnBehalf: null }).toPromise(); this.loading = false; if (result.result) { @@ -77,7 +77,7 @@ export class DiscoverCardDetailsComponent implements OnInit { 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.dialog.open(EpisodeRequestComponent, { width: "700px", data: {series: this.tv }, panelClass: 'modal-panel' }) } this.loading = false; diff --git a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.html b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.html index 771040f44..35eae0e21 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.html @@ -1,15 +1,30 @@ -
- - - {{result.title}} - - + -
{{result.title}}
-
{{result.title | truncate:20}}
-
- {{result.overview | truncate: 75}} +
+
+
+ {{RequestType[result.type] | humanize}} +
+
+ {{getAvailbilityStatus()}} +
+
+ {{result.title}} + +
+
+ +
- -
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.scss b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.scss index cfb8e6494..ec2dbae3b 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.scss +++ b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.scss @@ -2,8 +2,8 @@ $ombi-primary:#3f3f3f; $card-background: #2b2b2b; #cardImage { - border-radius: 5px 5px 0px 0px; - height: 75%; + border-radius: 5px; + object-fit:cover; } .dark-card { @@ -22,24 +22,6 @@ $card-background: #2b2b2b; font-weight: bold; } -$border-width: 3px; - -.available { - border-bottom: $border-width #1DE9B6 solid; -} - -.approved { - border-bottom: $border-width #ff5722 solid; -} - -.requested { - border-bottom: $border-width #ffd740 solid; -} - -.notrequested { - border-bottom: $border-width #303030 solid; -} - .expand { text-align: center; } @@ -78,10 +60,205 @@ small { font-size: 0.8rem; } -@media (min-width: 2000px) { - #cardImage { - height: 80%; - object-fit: cover; - display: block; - } +.ombi-card{ + height:100%; +} + #cardImage { + height: 100%; + object-fit: cover; + display: block; + } + + + +.box { + position: relative; + max-width: 600px; + } + +.ombi-card { + padding: 5px; +} +::ng-deep .p-carousel-indicators { + display: none !important; + } + + +.image { + border-radius: 10px; + opacity: 1; + display: block; + width: 100%; + height: auto; + transition: .5s ease; + backface-visibility: hidden; +} + + +.middle { + transition: .5s ease; + opacity: 0; + position: absolute; + top: 67%; + width: 90%; + left: 50%; + transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); +} + + +.c { + position: relative; +} + +.c:hover .image { + opacity: 0.3; +} + +.c:hover .middle { + opacity: 1; +} + +.small-text { + font-size: 11px; +} +.title { + font-size: 18px; + } + + +.full-width { + width: 100%; + } + +.ellipsis { + display: -webkit-box; + -webkit-line-clamp: 6; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-top-info{ + position: absolute; + text-transform: uppercase; + top: 0px; + width: 100%; + background-color: rgba(15,23,31,0.6); + display:flex; + justify-content: space-between; + padding-top:0.6em; + padding-bottom:0.3em; + z-index:2; +} + +.top-left { + font-size: 14px; + padding-left: 0.5em; + font-size: 14px; +} + +/* common */ +.top-right{ + display:flex; +} + +.top-right span.indicator, span.indicator-text { + display: none; + background-color: transparent; + color: #fff; + text-shadow: 0 1px 1px rgba(0,0,0,.2); + text-align: right; + font-size: 14px; +} + +.top-right span.indicator{ + padding-right: 0px; +} +.top-right span.indicator-text{ + padding-right: 1em; +} + +.top-right span.indicator:before{ + content: ''; + width: 10px; + height: 10px; + -moz-border-radius: 7.5px; + -webkit-border-radius: 7.5px; + border-radius: 7.5px; + margin-right:5px; +} + +.top-right.available span.indicator, span.indicator-text{ + display:block; +} + +.top-right.available span.indicator:before{ + display: inline-block; + background-color: #1DE9B6; +} + +.top-right.approved span.indicator, span.indicator-text { + display: block; +} + +.top-right.approved span.indicator:before{ + display: inline-block; + background-color: #ff5722; +} + +.top-right.requested span.indicator, span.indicator-text { + display: block; +} + +.top-right.requested span.indicator:before{ + display: inline-block; + background-color: #ffd740; +} + +::ng-deep a.poster-overlay{ + color:#fff; +} + +a.poster-overlay:hover{ + text-decoration: none; +} + +@media screen and (max-width: 400px){ + .ellipsis{ + display:none; + } + + .top-right span.indicator-text{ + display:none; + } + + .top-right span.indicator{ + padding-right:1em; + } +} + +.ombi-card #cardImage:hover{ + cursor: pointer; +} + +.ombi-card .button-request-container{ + position: relative; + width: 100%; + margin-left: 0; + margin-right: 0; + margin-top: -37px; + margin-bottom: 0px; + opacity:0; + transition: .5s ease; +} + +::ng-deep .ombi-card .button-request-container .button-request{ + padding-right:0px; + padding-left:0px; + width:100%; +} + +.c:hover .button-request-container { + opacity:1; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.ts b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.ts index cb401b8d1..1bb4f4359 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/card/discover-card.component.ts @@ -1,11 +1,12 @@ import { Component, OnInit, Input } from "@angular/core"; import { IDiscoverCardResult } from "../../interfaces"; -import { RequestType, ISearchTvResult, ISearchMovieResult } from "../../../interfaces"; -import { SearchV2Service } from "../../../services"; +import { RequestType } from "../../../interfaces"; +import { MessageService, RequestService, SearchV2Service } from "../../../services"; import { MatDialog } from "@angular/material/dialog"; import { DiscoverCardDetailsComponent } from "./discover-card-details.component"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; +import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; @Component({ selector: "discover-card", @@ -16,8 +17,15 @@ export class DiscoverCardComponent implements OnInit { @Input() public result: IDiscoverCardResult; public RequestType = RequestType; + public hide: boolean; + public fullyLoaded = false; + public loading: boolean; - constructor(private searchService: SearchV2Service, private dialog: MatDialog) { } + // This data is needed to open the dialog + private tvSearchResult: ISearchTvResultV2; + + constructor(private searchService: SearchV2Service, private dialog: MatDialog, private requestService: RequestService, + public messageService: MessageService) { } public ngOnInit() { if (this.result.type == RequestType.tvShow) { @@ -26,6 +34,9 @@ export class DiscoverCardComponent implements OnInit { if (this.result.type == RequestType.movie) { this.getExtraMovieInfo(); } + if (this.result.type == RequestType.album) { + this.getAlbumInformation(); + } } public openDetails(details: IDiscoverCardResult) { @@ -33,10 +44,49 @@ export class DiscoverCardComponent implements OnInit { } public async getExtraTvInfo() { - var result = await this.searchService.getTvInfo(this.result.id); - this.setTvDefaults(result); - this.updateTvItem(result); + if (this.result.tvMovieDb) { + this.tvSearchResult = await this.searchService.getTvInfoWithMovieDbId(+this.result.id); + } else { + this.tvSearchResult = await this.searchService.getTvInfo(+this.result.id); + } + if (!this.tvSearchResult || this.tvSearchResult?.status.length > 0 && this.tvSearchResult?.status === "404") { + this.hide = true; + return; + } + + this.setTvDefaults(this.tvSearchResult); + this.updateTvItem(this.tvSearchResult); + + } + public async getAlbumInformation() { + this.searchService.getArtistInformation(this.result.id.toString()).subscribe(x => { + if (x.poster) { + this.result.posterPath = x.poster; + this.fullyLoaded = true; + } else { + this.searchService.getReleaseGroupArt(this.result.id.toString()).subscribe(art => { + if (art.image) { + this.result.posterPath = art.image; + + this.fullyLoaded = true; + } + }) + } + this.result.title = x.startYear ? `${x.name} (${x.startYear})` : x.name; + this.result.overview = x.overview; + }); + } + + public generateDetailsLink(): string { + switch (this.result.type) { + case RequestType.movie: + return `/details/movie/${this.result.id}`; + case RequestType.tvShow: + return `/details/tv/${this.result.id}`; + case RequestType.album: //Actually artist + return `/details/artist/${this.result.id}`; + } } public getStatusClass(): string { @@ -49,15 +99,52 @@ export class DiscoverCardComponent implements OnInit { if (this.result.requested) { return "requested"; } - return "notrequested"; + return ""; + } + + public getAvailbilityStatus(): string { + if (this.result.available) { + return "Available"; + } + if (this.result.approved) { + return "Approved"; + } + if (this.result.requested) { + return "Pending"; + } + return ""; + } + + public request(event: any) { + event.preventDefault(); + this.loading = true; + switch (this.result.type) { + case RequestType.tvShow: + const dia = this.dialog.open(EpisodeRequestComponent, { width: "700px", data: { series: this.tvSearchResult }, panelClass: 'modal-panel' }); + dia.afterClosed().subscribe(x => this.loading = false); + return; + case RequestType.movie: + this.requestService.requestMovie({ theMovieDbId: +this.result.id, languageCode: null, requestOnBehalf: null }).subscribe(x => { + if (x.result) { + this.result.requested = true; + this.messageService.send(x.message, "Ok"); + } else { + this.messageService.send(x.errorMessage, "Ok"); + } + this.loading = false; + }); + return; + } } private getExtraMovieInfo() { if (!this.result.imdbid) { - this.searchService.getFullMovieDetails(this.result.id) + this.searchService.getFullMovieDetails(+this.result.id) .subscribe(m => { this.updateMovieItem(m); }); + } else { + this.fullyLoaded = true; } } @@ -67,24 +154,32 @@ export class DiscoverCardComponent implements OnInit { this.result.requested = updated.requested; this.result.requested = updated.requestProcessing; this.result.rating = updated.voteAverage; + this.result.overview = updated.overview; + this.result.imdbid = updated.imdbId; + + this.fullyLoaded = true; } private setTvDefaults(x: ISearchTvResultV2) { - if (!x.imdbId) { - x.imdbId = "https://www.tvmaze.com/shows/" + x.seriesId; - } else { + if (x.imdbId) { x.imdbId = "http://www.imdb.com/title/" + x.imdbId + "/"; + } else { + x.imdbId = "https://www.tvmaze.com/shows/" + x.seriesId; } } private updateTvItem(updated: ISearchTvResultV2) { this.result.title = updated.title; this.result.id = updated.id; - this.result.available = updated.fullyAvailable; + this.result.available = updated.fullyAvailable || updated.partlyAvailable; this.result.posterPath = updated.banner; this.result.requested = updated.requested; this.result.url = updated.imdbId; + this.result.overview = updated.overview; + this.result.approved = updated.approved; + + this.fullyLoaded = true; } } diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html new file mode 100644 index 000000000..4b7767721 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html @@ -0,0 +1,13 @@ +
+ + {{'Discovery.Combined' | translate}} + {{'Discovery.Movies' | translate}} + {{'Discovery.Tv' | translate}} + +
+ + + + + + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss new file mode 100644 index 000000000..325c03512 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss @@ -0,0 +1,105 @@ +@import "~styles/variables.scss"; + +.ombi-card { + padding: 5px; +} +::ng-deep .p-carousel-indicators { + display: none !important; + } + + +.image { + border-radius: 10px; + opacity: 1; + display: block; + width: 100%; + height: auto; + transition: .5s ease; + backface-visibility: hidden; + } + + +.middle { + transition: .5s ease; + opacity: 0; + position: absolute; + top: 75%; + width: 90%; + left: 50%; + transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + } + + + .c { + position: relative; + } + +.c:hover .image { + opacity: 0.3; + } + + .c:hover .middle { + opacity: 1; + } + +.small-text { + font-size: 11px; +} +.title { + font-size: 16px; +} +.top-left { + font-size: 14px; + position: absolute; + top: 8px; + left: 16px; +} + + +.right { + text-align: right; + margin-top:-61px; +} + +@media (max-width:520px){ + .right{ + margin-top:0px; + text-align: center;; + } +} + +.discover-filter-buttons-group { + background: $ombi-background-primary; + border: 1px solid $ombi-background-primary-accent; + border-radius: 30px; + color:#fff; + margin-bottom:10px; + margin-right: 30px; + + .discover-filter-button{ + background:inherit; + color:inherit; + padding:0 0px; + border-radius: 30px; + padding-left: 20px; + padding-right: 20px; + } + + ::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content{ + line-height:40px; + } + + .button-active{ + background:$ombi-active; + } + + +} +::ng-deep .discover-filter-button .mat-button-toggle-button:focus{ + outline:none; +} + +.card-skeleton { + padding: 5px; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts new file mode 100644 index 000000000..c3909512e --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts @@ -0,0 +1,330 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { DiscoverOption, IDiscoverCardResult } from "../../interfaces"; +import { ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces"; +import { SearchV2Service } from "../../../services"; +import { StorageService } from "../../../shared/storage/storage-service"; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { Carousel } from 'primeng/carousel'; + +export enum DiscoverType { + Upcoming, + Trending, + Popular, +} + +@Component({ + selector: "carousel-list", + templateUrl: "./carousel-list.component.html", + styleUrls: ["./carousel-list.component.scss"], +}) +export class CarouselListComponent implements OnInit { + + @Input() public discoverType: DiscoverType; + @ViewChild('carousel', {static: false}) carousel: Carousel; + + public DiscoverOption = DiscoverOption; + public discoverOptions: DiscoverOption = DiscoverOption.Combined; + public discoverResults: IDiscoverCardResult[] = []; + public movies: ISearchMovieResult[] = []; + public tvShows: ISearchTvResult[] = []; + public responsiveOptions: any; + public RequestType = RequestType; + public loadingFlag: boolean; + + get mediaTypeStorageKey() { + return "DiscoverOptions" + this.discoverType.toString(); + }; + private amountToLoad = 17; + private currentlyLoaded = 0; + + constructor(private searchService: SearchV2Service, + private storageService: StorageService) { + this.responsiveOptions = [ + { + breakpoint: '4000px', + numVisible: 17, + numScroll: 17 + }, + { + breakpoint: '3800px', + numVisible: 16, + numScroll: 16 + }, + { + breakpoint: '3600px', + numVisible: 15, + numScroll: 15 + }, + { + breakpoint: '3400px', + numVisible: 14, + numScroll: 14 + }, + { + breakpoint: '3200px', + numVisible: 13, + numScroll: 13 + }, + { + breakpoint: '3000px', + numVisible: 12, + numScroll: 12 + }, + { + breakpoint: '2800px', + numVisible: 11, + numScroll: 11 + }, + { + breakpoint: '2600px', + numVisible: 10, + numScroll: 10 + }, + { + breakpoint: '2400px', + numVisible: 9, + numScroll: 9 + }, + { + breakpoint: '2200px', + numVisible: 8, + numScroll: 8 + }, + { + breakpoint: '2000px', + numVisible: 7, + numScroll: 7 + }, + { + breakpoint: '1800px', + numVisible: 6, + numScroll: 6 + }, + { + breakpoint: '1650px', + numVisible: 5, + numScroll: 5 + }, + { + breakpoint: '1500px', + numVisible: 4, + numScroll: 4 + }, + { + breakpoint: '1250px', + numVisible: 3, + numScroll: 3 + }, + { + breakpoint: '768px', + numVisible: 2, + numScroll: 2 + }, + { + breakpoint: '480px', + numVisible: 1, + numScroll: 1 + } + ]; + } + + public async ngOnInit() { + this.currentlyLoaded = 0; + const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey); + if (localDiscoverOptions) { + this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]]; + } + + let currentIteration = 0; + while (this.discoverResults.length <= 14 && currentIteration <= 3) { + currentIteration++; + await this.loadData(); + } + } + + public async toggleChanged(event: MatButtonToggleChange) { + await this.switchDiscoverMode(event.value); + } + + public async newPage() { + // Note this is using the internal carousel APIs + // https://github.com/primefaces/primeng/blob/master/src/app/components/carousel/carousel.ts + var end = this.carousel._page >= (this.carousel.totalDots() - 1); + if (end) { + var moviePromise: Promise; + var tvPromise: Promise; + switch (+this.discoverOptions) { + case DiscoverOption.Combined: + moviePromise = this.loadMovies(); + tvPromise = this.loadTv(); + break; + case DiscoverOption.Movie: + moviePromise = this.loadMovies(); + break; + case DiscoverOption.Tv: + tvPromise = this.loadTv(); + break; + } + await moviePromise; + await tvPromise; + + this.createModel(); + } + } + + private async loadData() { + var moviePromise: Promise; + var tvPromise: Promise; + switch (+this.discoverOptions) { + case DiscoverOption.Combined: + moviePromise = this.loadMovies(); + tvPromise = this.loadTv(); + break; + case DiscoverOption.Movie: + moviePromise = this.loadMovies(); + break; + case DiscoverOption.Tv: + tvPromise = this.loadTv(); + break; + } + await moviePromise; + await tvPromise; + this.createInitialModel(); + } + + private async switchDiscoverMode(newMode: DiscoverOption) { + if (this.discoverOptions === newMode) { + return; + } + this.loading(); + this.currentlyLoaded = 0; + this.discoverOptions = +newMode; + this.storageService.save(this.mediaTypeStorageKey, newMode.toString()); + await this.loadData(); + this.finishLoading(); + } + + private async loadMovies() { + switch (this.discoverType) { + case DiscoverType.Popular: + this.movies = await this.searchService.popularMoviesByPage(this.currentlyLoaded, this.amountToLoad); + break; + case DiscoverType.Trending: + this.movies = await this.searchService.nowPlayingMoviesByPage(this.currentlyLoaded, this.amountToLoad); + break; + case DiscoverType.Upcoming: + this.movies = await this.searchService.upcomingMoviesByPage(this.currentlyLoaded, this.amountToLoad); + break + } + this.currentlyLoaded += this.amountToLoad; + } + + private async loadTv() { + switch (this.discoverType) { + case DiscoverType.Popular: + this.tvShows = await this.searchService.popularTvByPage(this.currentlyLoaded, this.amountToLoad); + break; + case DiscoverType.Trending: + this.tvShows = await this.searchService.trendingTvByPage(this.currentlyLoaded, this.amountToLoad); + break; + case DiscoverType.Upcoming: + this.tvShows = await this.searchService.anticipatedTvByPage(this.currentlyLoaded, this.amountToLoad); + break + } + this.currentlyLoaded += this.amountToLoad; + } + + private createInitialModel() { + this.clear(); + this.createModel(); + } + + private createModel() { + const tempResults = []; + + switch (+this.discoverOptions) { + case DiscoverOption.Combined: + tempResults.push(...this.mapMovieModel()); + tempResults.push(...this.mapTvModel()); + this.shuffle(tempResults); + break; + case DiscoverOption.Movie: + tempResults.push(...this.mapMovieModel()); + break; + case DiscoverOption.Tv: + tempResults.push(...this.mapTvModel()); + break; + } + + this.discoverResults.push(...tempResults); + this.carousel.ngAfterContentInit(); + + this.finishLoading(); + } + + private mapMovieModel(): IDiscoverCardResult[] { + const tempResults = []; + this.movies.forEach(m => { + tempResults.push({ + available: m.available, + posterPath: m.posterPath ? `https://image.tmdb.org/t/p/w500/${m.posterPath}` : "../../../images/default_movie_poster.png", + 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, + imdbid: m.imdbId, + denied: false, + background: m.backdropPath + }); + }); + return tempResults; + } + + private mapTvModel(): IDiscoverCardResult[] { + const tempResults = []; + 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 || m.partlyAvailable, + imdbid: m.imdbId, + denied: false, + background: m.background + }); + }); + return tempResults; + } + + private clear() { + this.discoverResults = []; + } + + 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 finishLoading() { + this.loadingFlag = false; + } + + +} diff --git a/src/Ombi/ClientApp/src/app/discover/components/collections/discover-collections.component.ts b/src/Ombi/ClientApp/src/app/discover/components/collections/discover-collections.component.ts index 9008dd8ca..dd634ef92 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/collections/discover-collections.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/collections/discover-collections.component.ts @@ -34,7 +34,7 @@ export class DiscoverCollectionsComponent implements OnInit { public async requestCollection() { await this.collection.collection.forEach(async (movie) => { - await this.requestService.requestMovie({theMovieDbId: movie.id, languageCode: null}).toPromise(); + await this.requestService.requestMovie({theMovieDbId: movie.id, languageCode: null, requestOnBehalf: null}).toPromise(); }); this.messageService.send("Requested Collection"); } diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html index f105b23ab..ee52f371c 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html @@ -1,42 +1,27 @@
-
-
- - dashboard - calendar_view_day - -
+
-
-
- - - +
+

Popular

+
+
- -
-
- - - +
+

Trending

+
+
-
-
-
- -
-
-
- + +
+

Upcoming

+
+
+
-
- -
-
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss index c2386ae8f..0951061a1 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss @@ -1,210 +1,17 @@ -.full-height { - height: 100%; +.section { + margin: 20px; } - -.small-middle-container { - margin: auto; - width: 85%; -} - -.small-padding { - padding-left: 20px; - padding-right: 20px; - margin-bottom: 28px; -} - -.loading-spinner { - margin: 10%; -} - -#scroller { - height: 100vh; - overflow: scroll; -} - -.small-space { - padding-top: 1%; -} - -.discover-layout { - position: absolute; - float: right; - margin-right: 36px; - z-index: 1; -} - -::ng-deep .mat-card-image { - height: 75%; - object-fit: cover; - display: block; -} - -.card-spacing { - height: 100%; -} - -.mat-card-content h6 { - overflow: hidden; - white-space: nowrap; - font-weight: 500; - font-size: 1.1rem; -} - -@media (min-width: 300px) { - - .small-middle-container { - margin: inherit; - } - - .col-xl-2 { - flex: 0 0 100%; - max-width: 100%; - min-width: 100%; - } - - .btn-group { - width: 100%; - } - - mat-button-base { - width: 100%; - } - - .col { - padding-right: 10px !important; - padding-left: 10px !important; - } - - .row { - margin-left: 0px; - } - - .small-padding { - padding-left: 5px !important; - padding-right: 0px !important; - height: 40em; - } - - ::ng-deep .mat-card-image { - height: 85% !important; - } -} - -@media (min-width: 600px) { - .justify-content-md-center { - justify-content: center !important; - } - - .small-middle-container { - width: auto; - } - - .btn-group { - width: auto; - } - - mat-button-base { - width: auto; - } - - ::ng-deep .mat-card-image { - height: 75% !important; - } -} - -@media (min-width: 660px) { - .col-xl-2 { - flex: 0 0 50%; - max-width: 50%; - min-width: 50%; - } - - .col { - padding-right: 15px !important; - padding-left: 15px !important; - } - - .small-padding { - padding-left: 20px !important; - padding-right: 20px !important; - height: auto; - } - - .row { - margin-left: 0px; - } - - .small-middle-container { - width: auto; - overflow: hidden; - } - - .btn-group { - width: auto; - } - - mat-button-base { - width: auto; +::ng-deep .p-carousel-indicators { + display: none !important; } -} - -@media (min-width: 870px) { - .col-xl-2 { - flex: 0 0 33.33333%; - max-width: 33.33333%; - min-width: 33.33333%; - } -} - -@media (min-width: 1100px) { - .col-xl-2 { - flex: 0 0 20%; - max-width: 25%; - min-width: 25%; - } -} - -@media (min-width: 1300px) { - .col-xl-2 { - flex: 0 0 18%; - max-width: 20%; - min-width: 20%; - } -} -@media (min-width: 1600px) { - .col-xl-2 { - flex: 0 0 16.66666667%; - max-width: 16.66666667%; - min-width: 16.66666667%; - } -} - -@media (min-width: 1900px) { - .col-xl-2 { - flex: 0 0 14.285713%; - max-width: 14.285713%; - min-width: 14.285713%; - } -} - -@media (min-width: 2200px) { - .col-xl-2 { - flex: 0 0 12.5%; - max-width: 12.5%; - min-width: 12.5%; - } +h2{ + margin-top:40px; + margin-left:40px; + font-size: 24px; } -@media (min-width: 2500px) { - .col-xl-2 { - flex: 0 0 11.111111%; - max-width: 11.111111%; - min-width: 11.111111%; - } -} -@media (max-width: 420px) { - .discover-layout{ - display: none; - } +::ng-deep .p-carousel-item{ + min-height:290px; + max-height:290px; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts index c42a0417d..7b4fc291b 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts @@ -5,6 +5,8 @@ import { IDiscoverCardResult, DiscoverOption, DisplayOption } from "../../interf import { trigger, transition, style, animate } from "@angular/animations"; import { StorageService } from "../../../shared/storage/storage-service"; import { DOCUMENT } from "@angular/common"; +import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; +import { DiscoverType } from "../carousel-list/carousel-list.component"; @Component({ templateUrl: "./discover.component.html", @@ -20,11 +22,16 @@ import { DOCUMENT } from "@angular/common"; }) export class DiscoverComponent implements OnInit { + public upcomingMovies: IDiscoverCardResult[] = []; + public trendingMovies: IDiscoverCardResult[] = []; + + public discoverResults: IDiscoverCardResult[] = []; public movies: ISearchMovieResult[] = []; public tvShows: ISearchTvResult[] = []; public discoverOptions: DiscoverOption = DiscoverOption.Combined; + public DiscoverType = DiscoverType; public DiscoverOption = DiscoverOption; public displayOption: DisplayOption = DisplayOption.Card; public DisplayOption = DisplayOption; @@ -45,6 +52,8 @@ export class DiscoverComponent implements OnInit { private mediaTypeStorageKey = "DiscoverOptions"; private displayOptionsKey = "DiscoverDisplayOptions"; + + constructor(private searchService: SearchV2Service, private storageService: StorageService, @Inject(DOCUMENT) private container: Document) { } @@ -52,35 +61,38 @@ export class DiscoverComponent implements OnInit { public async ngOnInit() { this.loading() - const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey); - if (localDiscoverOptions) { - this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]]; - } - const localDisplayOptions = +this.storageService.get(this.displayOptionsKey); - if (localDisplayOptions) { - this.displayOption = DisplayOption[DisplayOption[localDisplayOptions]]; - } - this.scrollDisabled = true; - switch (this.discoverOptions) { - case DiscoverOption.Combined: - this.movies = await this.searchService.popularMoviesByPage(0, this.amountToLoad); - this.tvShows = await this.searchService.popularTvByPage(0, this.amountToLoad); - break; - case DiscoverOption.Movie: - this.movies = await this.searchService.popularMoviesByPage(0, this.amountToLoad); - break; - case DiscoverOption.Tv: - this.tvShows = await this.searchService.popularTvByPage(0, this.amountToLoad); - break; - } - - this.contentLoaded = this.amountToLoad; - - this.createInitialModel(); - this.scrollDisabled = false; - if (!this.containerHasScrollBar()) { - await this.onScroll(); - } + // this.upcomingMovies = this.mapTvModel(await this.searchService.popularTvByPage(0, 14)); + // this.trendingMovies = this.mapMovieModel(await this.searchService.popularMoviesByPage(0, 14)); +this.finishLoading(); + // const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey); + // if (localDiscoverOptions) { + // this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]]; + // } + // const localDisplayOptions = +this.storageService.get(this.displayOptionsKey); + // if (localDisplayOptions) { + // this.displayOption = DisplayOption[DisplayOption[localDisplayOptions]]; + // } + // this.scrollDisabled = true; + // switch (this.discoverOptions) { + // case DiscoverOption.Combined: + // this.movies = await this.searchService.popularMoviesByPage(0, this.amountToLoad); + // this.tvShows = await this.searchService.popularTvByPage(0, this.amountToLoad); + // break; + // case DiscoverOption.Movie: + // this.movies = await this.searchService.popularMoviesByPage(0, this.amountToLoad); + // break; + // case DiscoverOption.Tv: + // this.tvShows = await this.searchService.popularTvByPage(0, this.amountToLoad); + // break; + // } + + // this.contentLoaded = this.amountToLoad; + + // this.createInitialModel(); + // this.scrollDisabled = false; + // if (!this.containerHasScrollBar()) { + // await this.onScroll(); + // } } public async onScroll() { @@ -236,18 +248,18 @@ export class DiscoverComponent implements OnInit { private createModel() { const tempResults = []; - switch (this.discoverOptions) { - case DiscoverOption.Combined: - tempResults.push(...this.mapMovieModel()); - tempResults.push(...this.mapTvModel()); - break; - case DiscoverOption.Movie: - tempResults.push(...this.mapMovieModel()); - break; - case DiscoverOption.Tv: - tempResults.push(...this.mapTvModel()); - break; - } + // switch (this.discoverOptions) { + // case DiscoverOption.Combined: + // tempResults.push(...this.mapMovieModel()); + // tempResults.push(...this.mapTvModel()); + // break; + // case DiscoverOption.Movie: + // tempResults.push(...this.mapMovieModel()); + // break; + // case DiscoverOption.Tv: + // tempResults.push(...this.mapTvModel()); + // break; + // } this.shuffle(tempResults); this.discoverResults.push(...tempResults); @@ -255,9 +267,9 @@ export class DiscoverComponent implements OnInit { this.finishLoading(); } - private mapMovieModel(): IDiscoverCardResult[] { + private mapMovieModel(movies: ISearchMovieResult[]): IDiscoverCardResult[] { const tempResults = []; - this.movies.forEach(m => { + movies.forEach(m => { tempResults.push({ available: m.available, posterPath: m.posterPath ? `https://image.tmdb.org/t/p/w500/${m.posterPath}` : "../../../images/default_movie_poster.png", @@ -277,9 +289,9 @@ export class DiscoverComponent implements OnInit { return tempResults; } - private mapTvModel(): IDiscoverCardResult[] { + private mapTvModel(tv: ISearchTvResult[]): IDiscoverCardResult[] { const tempResults = []; - this.tvShows.forEach(m => { + tv.forEach(m => { tempResults.push({ available: m.available, posterPath: "../../../images/default_tv_poster.png", @@ -290,7 +302,7 @@ export class DiscoverComponent implements OnInit { url: undefined, rating: +m.rating, overview: m.overview, - approved: m.approved, + approved: m.approved || m.partlyAvailable, imdbid: m.imdbId, denied: false, background: m.background diff --git a/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.html b/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.html index aac17c79c..959538444 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.html @@ -50,6 +50,10 @@ play_circle_outline + + + play_circle_outline + @@ -59,6 +63,10 @@ play_circle_outline + + + play_circle_outline +
@@ -110,14 +118,14 @@ *ngIf="movie.requested || movie.approved; then requestedBtn else notRequestedBtn"> @@ -127,7 +135,7 @@ @@ -154,4 +165,4 @@
-
\ No newline at end of file +
diff --git a/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.ts b/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.ts index 4c28c1174..51ea7a85f 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/grid/discover-grid.component.ts @@ -5,7 +5,7 @@ import { ImageService, RequestService, SearchV2Service } from "../../../services import { MatDialog } from "@angular/material/dialog"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; -import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; +import { EpisodeRequestComponent, EpisodeRequestData } from "../../../shared/episode-request/episode-request.component"; import { MatSnackBar } from "@angular/material/snack-bar"; import { Router } from "@angular/router"; import { DomSanitizer } from "@angular/platform-browser"; @@ -27,8 +27,8 @@ export class DiscoverGridComponent implements OnInit { public movie: ISearchMovieResultV2; constructor(private searchService: SearchV2Service, private dialog: MatDialog, - private requestService: RequestService, private notification: MatSnackBar, - private router: Router, private sanitizer: DomSanitizer, private imageService: ImageService) { } + private requestService: RequestService, private notification: MatSnackBar, + private router: Router, private sanitizer: DomSanitizer, private imageService: ImageService) { } public ngOnInit() { if (this.result.type == RequestType.tvShow) { @@ -40,7 +40,7 @@ export class DiscoverGridComponent implements OnInit { } public async getExtraTvInfo() { - this.tv = await this.searchService.getTvInfo(this.result.id); + this.tv = await this.searchService.getTvInfo(+this.result.id); this.setTvDefaults(this.tv); this.updateTvItem(this.tv); const creator = this.tv.crew.filter(tv => { @@ -80,35 +80,33 @@ export class DiscoverGridComponent implements OnInit { } private getExtraMovieInfo() { - // if (!this.result.imdbid) { - this.searchService.getFullMovieDetails(this.result.id) - .subscribe(m => { - this.movie = m; - this.updateMovieItem(m); - }); - - this.setMovieBackground() - // } + this.searchService.getFullMovieDetails(+this.result.id) + .subscribe(m => { + this.movie = m; + this.updateMovieItem(m); + }); + + this.setMovieBackground() } private setMovieBackground(): void { - this.result.background = this.sanitizer.bypassSecurityTrustStyle - ("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(" + "https://image.tmdb.org/t/p/original" + this.result.background + ")"); - } + this.result.background = this.sanitizer.bypassSecurityTrustStyle + ("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(" + "https://image.tmdb.org/t/p/original" + this.result.background + ")"); + } - private setTvBackground(): void { - if (this.result.background != null) { - this.result.background = this.sanitizer.bypassSecurityTrustStyle - ("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(https://image.tmdb.org/t/p/original" + this.result.background + ")"); - } else { - this.imageService.getTvBanner(this.result.id).subscribe(x => { - if (x) { - this.result.background = this.sanitizer.bypassSecurityTrustStyle - ("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(" + x + ")"); - } - }); - } - } + private setTvBackground(): void { + if (this.result.background != null) { + this.result.background = this.sanitizer.bypassSecurityTrustStyle + ("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(https://image.tmdb.org/t/p/original" + this.result.background + ")"); + } else { + this.imageService.getTvBanner(+this.result.id).subscribe(x => { + if (x) { + this.result.background = this.sanitizer.bypassSecurityTrustStyle + ("linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(" + x + ")"); + } + }); + } + } private updateMovieItem(updated: ISearchMovieResultV2) { this.result.url = "http://www.imdb.com/title/" + updated.imdbId + "/"; @@ -139,7 +137,7 @@ export class DiscoverGridComponent implements OnInit { public async request() { this.requesting = true; if (this.result.type === RequestType.movie) { - const result = await this.requestService.requestMovie({ theMovieDbId: this.result.id, languageCode: "" }).toPromise(); + const result = await this.requestService.requestMovie({ theMovieDbId: +this.result.id, languageCode: "", requestOnBehalf: null }).toPromise(); if (result.result) { this.result.requested = true; @@ -148,7 +146,7 @@ export class DiscoverGridComponent implements OnInit { this.notification.open(result.errorMessage, "Ok"); } } else if (this.result.type === RequestType.tvShow) { - this.dialog.open(EpisodeRequestComponent, { width: "700px", data: this.tv, panelClass: 'modal-panel' }) + this.dialog.open(EpisodeRequestComponent, { width: "700px", data: { series: this.tv, requestOnBehalf: null }, panelClass: 'modal-panel' }) } this.requesting = false; } diff --git a/src/Ombi/ClientApp/src/app/discover/components/index.ts b/src/Ombi/ClientApp/src/app/discover/components/index.ts index e65e48bd3..c0f3a7141 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/index.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/index.ts @@ -8,6 +8,8 @@ import { AuthGuard } from "../../auth/auth.guard"; import { SearchService, RequestService } from "../../services"; import { MatDialog } from "@angular/material/dialog"; import { DiscoverGridComponent } from "./grid/discover-grid.component"; +import { DiscoverSearchResultsComponent } from "./search-results/search-results.component"; +import { CarouselListComponent } from "./carousel-list/carousel-list.component"; export const components: any[] = [ @@ -17,6 +19,8 @@ export const components: any[] = [ DiscoverCollectionsComponent, DiscoverActorComponent, DiscoverGridComponent, + DiscoverSearchResultsComponent, + CarouselListComponent, ]; @@ -33,5 +37,6 @@ export const providers: any[] = [ export const routes: Routes = [ { path: "", component: DiscoverComponent, canActivate: [AuthGuard] }, { path: "collection/:collectionId", component: DiscoverCollectionsComponent, canActivate: [AuthGuard] }, - { path: "actor/:actorId", component: DiscoverActorComponent, canActivate: [AuthGuard] } + { path: "actor/:actorId", component: DiscoverActorComponent, canActivate: [AuthGuard] }, + { path: ":searchTerm", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] }, ]; \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html new file mode 100644 index 000000000..5a8ad4b42 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html @@ -0,0 +1,15 @@ +
+
+ +
+
+
+ +
+
+
+
+

{{'Discovery.NoSearch' | translate}}

+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.scss b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.scss new file mode 100644 index 000000000..728ff23c5 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.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/components/search-results/search-results.component.ts b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts new file mode 100644 index 000000000..0b2aa2030 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { SearchV2Service } from "../../../services"; +import { IDiscoverCardResult } from "../../interfaces"; +import { IMultiSearchResult, RequestType } from "../../../interfaces"; +import { FilterService } from "../../services/filter-service"; +import { SearchFilter } from "../../../my-nav/SearchFilter"; +import { StorageService } from "../../../shared/storage/storage-service"; + +import { isEqual } from "lodash"; + +@Component({ + templateUrl: "./search-results.component.html", + styleUrls: ["../discover/discover.component.scss"], +}) +export class DiscoverSearchResultsComponent implements OnInit { + + public loadingFlag: boolean; + public searchTerm: string; + public results: IMultiSearchResult[]; + + public discoverResults: IDiscoverCardResult[] = []; + + public filter: SearchFilter; + + constructor(private searchService: SearchV2Service, + private route: ActivatedRoute, + private filterService: FilterService, + private store: StorageService) { + this.route.params.subscribe((params: any) => { + this.searchTerm = params.searchTerm; + this.clear(); + this.init(); + }); + } + + public async ngOnInit() { + this.loadingFlag = true; + + this.filterService.onFilterChange.subscribe(async x => { + if (!isEqual(this.filter, x)) { + this.filter = { ...x }; + await this.search(); + } + }); + } + + public async init() { + var filter = this.store.get("searchFilter"); + if (filter) { + this.filter = Object.assign(new SearchFilter(), JSON.parse(filter)); + } else { + this.filter = new SearchFilter({ movies: true, tvShows: true, people: false, music: false }); + } + this.loading(); + await this.search(); + } + + public createInitalModel() { + this.finishLoading(); + this.results.forEach(m => { + + let mediaType = RequestType.movie; + if (m.mediaType == "movie") { + mediaType = RequestType.movie; + } else if (m.mediaType == "tv") { + mediaType = RequestType.tvShow; + } else if (m.mediaType == "Artist") { + mediaType = RequestType.album; + } + + let poster = `https://image.tmdb.org/t/p/w300/${m.poster}`; + if (!m.poster) { + if (mediaType === RequestType.movie) { + poster = "images/default_movie_poster.png" + } + if (mediaType === RequestType.tvShow) { + poster = "images/default_tv_poster.png" + } + } + + this.discoverResults.push({ + posterPath: mediaType !== RequestType.album ? poster : "images/default-music-placeholder.png", + requested: false, + title: m.title, + type: mediaType, + id: m.id, + url: "", + rating: 0, + overview: "", + approved: false, + imdbid: "", + denied: false, + background: "", + available: false, + tvMovieDb: mediaType === RequestType.tvShow ? true : false + }); + }); + } + + private loading() { + this.loadingFlag = true; + } + + private finishLoading() { + this.loadingFlag = false; + } + + private clear() { + this.results = []; + this.discoverResults = []; + } + + private async search() { + this.clear(); + this.results = await this.searchService + .multiSearch(this.searchTerm, this.filter).toPromise(); + this.createInitalModel(); + } +} diff --git a/src/Ombi/ClientApp/src/app/discover/discover.module.ts b/src/Ombi/ClientApp/src/app/discover/discover.module.ts index e40cf102a..931a0861b 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.module.ts +++ b/src/Ombi/ClientApp/src/app/discover/discover.module.ts @@ -5,6 +5,8 @@ import {MatButtonToggleModule} from '@angular/material/button-toggle'; import { SharedModule } from "../shared/shared.module"; import { PipeModule } from "../pipes/pipe.module"; +import { CarouselModule } from 'primeng/carousel'; +import { SkeletonModule } from 'primeng/skeleton'; import * as fromComponents from './components'; @@ -14,8 +16,10 @@ import * as fromComponents from './components'; RouterModule.forChild(fromComponents.routes), SharedModule, PipeModule, + CarouselModule, MatButtonToggleModule, InfiniteScrollModule, + SkeletonModule, ], declarations: [ ...fromComponents.components diff --git a/src/Ombi/ClientApp/src/app/discover/interfaces.ts b/src/Ombi/ClientApp/src/app/discover/interfaces.ts index 8a3276a2d..7127f52c5 100644 --- a/src/Ombi/ClientApp/src/app/discover/interfaces.ts +++ b/src/Ombi/ClientApp/src/app/discover/interfaces.ts @@ -1,7 +1,7 @@ import { RequestType } from "../interfaces"; export interface IDiscoverCardResult { - id: number; + id: number | string; posterPath: string; url: string | undefined; title: string; @@ -14,6 +14,8 @@ export interface IDiscoverCardResult { overview: string; imdbid: string; background: string|any; + + tvMovieDb?: boolean; } export enum DiscoverOption { diff --git a/src/Ombi/ClientApp/src/app/discover/services/filter-service.ts b/src/Ombi/ClientApp/src/app/discover/services/filter-service.ts new file mode 100644 index 000000000..bceeb05d6 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/discover/services/filter-service.ts @@ -0,0 +1,14 @@ +import { EventEmitter, Injectable, Output } from "@angular/core"; +import { SearchFilter } from "../../my-nav/SearchFilter"; + +@Injectable() +export class FilterService { + + @Output() public onFilterChange = new EventEmitter(); + public filter: SearchFilter; + + public changeFilter(filter: SearchFilter) { + this.filter = filter; + this.onFilterChange.emit(this.filter); + } +} diff --git a/src/Ombi/ClientApp/src/app/interfaces/IPlex.ts b/src/Ombi/ClientApp/src/app/interfaces/IPlex.ts index de3fc8cf1..6afc13e21 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IPlex.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IPlex.ts @@ -14,6 +14,8 @@ export interface IPlexOAuthViewModel { export interface IPlexOAuthAccessToken { accessToken: string; + success: boolean; + error: string; } export interface IPlexUser { diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts b/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts index a52b19d9d..e01d74cd5 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRadarr.ts @@ -1,4 +1,7 @@ -export interface IRadarrRootFolder { +import { IChildRequests, IMovieRequests } from "."; +import { ITvRequests } from "./IRequestModel"; + +export interface IRadarrRootFolder { id: number; path: string; } @@ -24,4 +27,6 @@ export interface IAdvancedData { rootFolder: IRadarrRootFolder; rootFolders: IRadarrRootFolder[]; rootFolderId: number; + movieRequest: IMovieRequests; + tvRequest: ITvRequests; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRatings.ts b/src/Ombi/ClientApp/src/app/interfaces/IRatings.ts new file mode 100644 index 000000000..fe7a86614 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/interfaces/IRatings.ts @@ -0,0 +1,11 @@ +export interface IMovieRatings { + critics_rating: string; + critics_score: number; + audience_rating: string; + audience_score: number; +} + +export interface ITvRatings { + class: string; + score: number; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRecentlyAdded.ts b/src/Ombi/ClientApp/src/app/interfaces/IRecentlyAdded.ts index fc358eb0d..725f3b3ac 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRecentlyAdded.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRecentlyAdded.ts @@ -26,4 +26,5 @@ export interface IRecentlyAddedRangeModel { export enum RecentlyAddedType { Plex, Emby, + Jellyfin, } diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts index 4efb5ee76..1ee92f336 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts @@ -167,6 +167,7 @@ export interface IEpisodesRequests { export interface IMovieRequestModel { theMovieDbId: number; languageCode: string | undefined; + requestOnBehalf: string | undefined; } export interface IFilter { diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResult.ts b/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResult.ts index f2460714f..70216896c 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResult.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResult.ts @@ -23,6 +23,7 @@ available: boolean; plexUrl: string; embyUrl: string; + jellyfinUrl: string; quality: string; digitalReleaseDate: Date; subscribed: boolean; diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts b/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts index 5787a363a..da4b27f32 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISearchMovieResultV2.ts @@ -34,6 +34,7 @@ recommendations: IOtherMovies; plexUrl: string; embyUrl: string; + jellyfinUrl: string; quality: string; digitalReleaseDate: Date; subscribed: boolean; @@ -166,4 +167,4 @@ export interface IOtherMoviesViewModel { vote_average: number; vote_count: number; popularity: number; -} \ No newline at end of file +} diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResult.ts b/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResult.ts index 477cc6dcd..78438a079 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResult.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResult.ts @@ -28,6 +28,7 @@ export interface ISearchTvResult { available: boolean; plexUrl: string; embyUrl: string; + jellyfinUrl: string; quality: string; firstSeason: boolean; latestSeason: boolean; @@ -46,6 +47,7 @@ export interface ITvRequestViewModel { latestSeason: boolean; tvDbId: number; seasons: ISeasonsViewModel[]; + requestOnBehalf: string | undefined; } export interface ISeasonsViewModel { diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts b/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts index 0d37acd71..ef48adc21 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISearchTvResultV2.ts @@ -29,6 +29,7 @@ export interface ISearchTvResultV2 { available: boolean; plexUrl: string; embyUrl: string; + jellyfinUrl: string; quality: string; firstSeason: boolean; latestSeason: boolean; @@ -62,6 +63,7 @@ export interface IMovieCollection { available: boolean; plexUrl: string; embyUrl: string; + jellyfinUrl: string; imdbId: string; } diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts index 0656bb1db..c5b29adff 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts @@ -36,7 +36,6 @@ export interface IUpdateSettings extends ISettings { export interface IEmbySettings extends ISettings { enable: boolean; - isJellyfin: boolean; servers: IEmbyServer[]; } @@ -52,7 +51,25 @@ export interface IEmbyServer extends IExternalSettings { export interface IPublicInfo { id: string; serverName: string; - isJellyfin: boolean; +} + +export interface IJellyfinSettings extends ISettings { + enable: boolean; + servers: IJellyfinServer[]; +} + +export interface IJellyfinServer extends IExternalSettings { + serverId: string; + name: string; + apiKey: string; + administratorId: string; + enableEpisodeSearching: boolean; + serverHostname: string; +} + +export interface IPublicInfo { + id: string; + serverName: string; } export interface IPlexSettings extends ISettings { @@ -141,6 +158,7 @@ export interface ICustomizationSettings extends ISettings { export interface IJobSettings { embyContentSync: string; + jellyfinContentSync: string; sonarrSync: string; radarrSync: string; plexContentSync: string; @@ -187,11 +205,14 @@ export interface IUserManagementSettings extends ISettings { importPlexUsers: boolean; importPlexAdmin: boolean; importEmbyUsers: boolean; + importJellyfinUsers: boolean; defaultRoles: string[]; movieRequestLimit: number; episodeRequestLimit: number; bannedPlexUserIds: string[]; bannedEmbyUserIds: string[]; + bannedJellyfinUserIds: string[]; + defaultStreamingCountry: string; } export interface IAbout { diff --git a/src/Ombi/ClientApp/src/app/interfaces/IStreams.ts b/src/Ombi/ClientApp/src/app/interfaces/IStreams.ts new file mode 100644 index 000000000..b666e01f6 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/interfaces/IStreams.ts @@ -0,0 +1,5 @@ +export interface IStreamingData { + order: number; + streamingProvider: string; + logo: string; +} diff --git a/src/Ombi/ClientApp/src/app/interfaces/ITester.ts b/src/Ombi/ClientApp/src/app/interfaces/ITester.ts new file mode 100644 index 000000000..c61292be4 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/interfaces/ITester.ts @@ -0,0 +1,4 @@ +export interface ITesterResult { + isValid: boolean; + expectedSubDir?: string; +} diff --git a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts index 4235d55cf..856d57f0a 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts @@ -17,6 +17,7 @@ export interface IUser { userAccessToken: string; language: string; userQualityProfiles: IUserQualityProfiles; + streamingCountry: string; // FOR UI episodeRequestQuota: IRemainingRequests | null; @@ -24,13 +25,18 @@ export interface IUser { musicRequestQuota: IRemainingRequests | null; } +export interface IUserDropdown { + username: string; + id: string; +} + export interface IUserQualityProfiles { sonarrQualityProfileAnime: number; sonarrRootPathAnime: number; sonarrRootPath: number; sonarrQualityProfile: number; radarrRootPath: number; - radarrQualityProfile: number; + radarrQualityProfile: number; } export interface ICreateWizardUser { @@ -44,10 +50,16 @@ export interface IWizardUserResult { errors: string[]; } +export interface IStreamingCountries { + code: string; +} + export enum UserType { LocalUser = 1, PlexUser = 2, EmbyUser = 3, + EmbyConnect = 4, + JellyfinUser = 5, } export interface IIdentityResult { diff --git a/src/Ombi/ClientApp/src/app/interfaces/index.ts b/src/Ombi/ClientApp/src/app/interfaces/index.ts index 8c0bf9cad..40916cd00 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/index.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/index.ts @@ -1,4 +1,4 @@ -export * from "./ICommon"; +export * from "./ICommon"; export * from "./ICouchPotato"; export * from "./IImages"; export * from "./IMediaServerStatus"; @@ -20,3 +20,4 @@ export * from "./ISearchMusicResult"; export * from "./IVote"; export * from "./IFailedRequests"; export * from "./IHub"; +export * from "./ITester"; diff --git a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html index 44be113c6..c2de76c2b 100644 --- a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html @@ -61,7 +61,7 @@
-
+

{{comment.comment}}

diff --git a/src/Ombi/ClientApp/src/app/issues/issues.component.ts b/src/Ombi/ClientApp/src/app/issues/issues.component.ts index c97eb4eff..b8482f1ff 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.component.ts @@ -4,6 +4,8 @@ import { IssuesService } from "../services"; import { IIssueCount, IIssues, IPagenator, IssueStatus } from "../interfaces"; +import { PageEvent } from '@angular/material/paginator'; + @Component({ templateUrl: "issues.component.html", styleUrls: ['issues.component.scss'] @@ -16,7 +18,7 @@ export class IssuesComponent implements OnInit { public count: IIssueCount; - private takeAmount = 10; + private takeAmount = 50; private pendingSkip = 0; private inProgressSkip = 0; private resolvedSkip = 0; @@ -30,18 +32,18 @@ export class IssuesComponent implements OnInit { this.issueService.getIssuesCount().subscribe(x => this.count = x); } - public changePagePending(event: IPagenator) { - this.pendingSkip = event.first; + public changePagePending(event: PageEvent) { + this.pendingSkip = event.pageSize * event.pageIndex++; this.getPending(); } - public changePageInProg(event: IPagenator) { - this.inProgressSkip = event.first; + public changePageInProg(event: PageEvent) { + this.inProgressSkip = event.pageSize * event.pageIndex++; this.getInProg(); } - public changePageResolved(event: IPagenator) { - this.resolvedSkip = event.first; + public changePageResolved(event: PageEvent) { + this.resolvedSkip = event.pageSize * event.pageIndex++; this.getResolved(); } diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html index 8713d09b2..e266a718a 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html @@ -26,94 +26,17 @@ {{element.userReported.userAlias}} - - - - + - \ No newline at end of file + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts index d1c656662..ff584a35e 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts @@ -14,7 +14,9 @@ export class IssuesTableComponent { @Output() public changePage = new EventEmitter(); public displayedColumns = ["title", "category", "subject", "status", "reportedBy", "actions"] - public IssueStatus = IssueStatus; + public IssueStatus = IssueStatus; + public resultsLength: number; + public gridCount: string = "15"; public order: string = "id"; public reverse = false; @@ -44,11 +46,6 @@ export class IssuesTableComponent { } public paginate(event: IPagenator) { - //event.first = Index of the first record (current index) - //event.rows = Number of rows to display in new page - //event.page = Index of the new page - //event.pageCount = Total number of pages - this.changePage.emit(event); } diff --git a/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.html b/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.html index 298ce4c11..7f5d13c3c 100644 --- a/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.html +++ b/src/Ombi/ClientApp/src/app/landingpage/landingpage.component.html @@ -13,21 +13,21 @@
-

 Notice

+

 Notice


- +
-

{{ 'LandingPage.OnlineHeading' | translate }}

+

{{ 'LandingPage.OnlineHeading' | translate }}

-

{{ 'LandingPage.PartiallyOnlineHeading' | translate }}

+

{{ 'LandingPage.PartiallyOnlineHeading' | translate }}

There is {{mediaServerStatus.serversUnavailable}} server offline out of {{mediaServerStatus.totalServers}}.

@@ -35,7 +35,7 @@
-

{{ 'LandingPage.OfflineHeading' | translate }}

+

{{ 'LandingPage.OfflineHeading' | translate }}

diff --git a/src/Ombi/ClientApp/src/app/login/login.component.html b/src/Ombi/ClientApp/src/app/login/login.component.html index 27b8d827a..b0f4841fa 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.html +++ b/src/Ombi/ClientApp/src/app/login/login.component.html @@ -1,10 +1,12 @@ 
+
diff --git a/src/Ombi/ClientApp/src/app/login/login.component.scss b/src/Ombi/ClientApp/src/app/login/login.component.scss index aa285a957..9eacab721 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.scss +++ b/src/Ombi/ClientApp/src/app/login/login.component.scss @@ -1,19 +1,8 @@ -$primary-colour: #df691a; -$primary-colour-outline: #ff761b; -$bg-colour: #333333; -$bg-colour-disabled: #252424; -$form-color: #4e5d6c; -$form-color-lighter: #637689; -$info-colour: #5bc0de; -$warning-colour: #f0ad4e; -$danger-colour: #d9534f; -$success-colour: #5cb85c; -$placeholder-colour: #3b3b3b; +@import "~styles/variables.scss"; body, html { height: 100% !important; background-repeat: no-repeat !important; - background-image: linear-gradient(rgb(104, 145, 162), rgb(12, 97, 33)) !important; } img.center { @@ -22,6 +11,13 @@ img.center { max-width: 100%; } +.login-gradient-bar{ + background: linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.6) 20.0%, rgba(0,0,0,0.6) 80.0%, transparent 60%),transparent; + height:100%; + width:100%; + position: absolute; +} + div.bg { background-position: center center; background-repeat: no-repeat; @@ -57,24 +53,6 @@ div.bg { user-select: none; cursor: default; } -/* - * Card component - */ -.card { - /*background-image: linear-gradient(rgb(104, 145, 162), rgb(12, 97, 33)) !important;*/ - background-color: $bg-colour; - /* just in case there no content*/ - padding: 20px 25px 30px; - margin: 0 auto 25px; - margin-top: 50px; - /* shadows and rounded borders */ - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; - /*-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); - box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);*/ -} .profile-img-custom { width: 100%; @@ -93,18 +71,6 @@ div.bg { /* * Form styles */ -.profile-name-card { - font-size: 16px; - font-weight: bold; - text-align: center; - margin: 10px 0 0; - min-height: 1em; -} - -.form-control { - color: black; - background-color: white !important; -} .reauth-email { display: block; @@ -178,32 +144,6 @@ div.bg { color: rgb(12, 97, 33); } - -// Placeholders - -::-webkit-input-placeholder { /* WebKit, Blink, Edge */ - color: $placeholder-colour; -} - -:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ - color: $placeholder-colour; - opacity: 1; -} - -::-moz-placeholder { /* Mozilla Firefox 19+ */ - color: $placeholder-colour; - opacity: 1; -} - -:-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: $placeholder-colour; -} - -::-ms-input-placeholder { /* Microsoft Edge */ - color: $placeholder-colour; -} - - .full-width { width: 100%; } @@ -212,23 +152,63 @@ div.bg { .small-middle-container{ margin: auto; overflow: auto; + width:auto; } } @media (min-width: 571px) { .small-middle-container{ margin: auto; - width: 25%; overflow: auto; } } -.top-margin { - margin-top:10%; -} - .login-buttons { text-align: center; } -.login-card { - background: #424242; + +::ng-deep button#sign-in{ + color: $ombi-active-text; +} + +::ng-deep .login-card .mat-form-field-appearance-outline.mat-focused .mat-form-field-outline-thick{ + color: $ombi-active; +} + +::ng-deep .login-card .mat-form-field.mat-focused .mat-form-field-label{ + color: $ombi-active; +} + +::ng-deep .login-card .mat-input-element{ + caret-color: $ombi-active; +} + +.small-middle-container{ + display:flex; + align-items: center; + justify-content: center; + height:100vh; +} + +.login-card.mat-card{ + background-color: rgba(0,0,0,0.6); + padding:60px 40px; +} + +.login-card H1.login_logo{ + display:flex; + font-family: Montserrat,sans-serif; + text-transform: uppercase; + color:$ombi-active; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 10em; + width:100%; + margin-bottom:70px; +} + +@media (max-width: 600px){ + .login-card H1.login_logo{ + font-size:20vw; + } } \ 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 bdb57d403..5a85783ae 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.ts +++ b/src/Ombi/ClientApp/src/app/login/login.component.ts @@ -73,7 +73,7 @@ export class LoginComponent implements OnDestroy, OnInit { }); this.form = this.fb.group({ - username: ["", [Validators.required]], + username: [""], password: [""], rememberMe: [false], }); @@ -94,7 +94,7 @@ export class LoginComponent implements OnDestroy, OnInit { this.settingsService.getClientId().subscribe(x => this.clientId = x); this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); 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 + ")"); + this.background = this.sanitizer.bypassSecurityTrustStyle("url(" + x.url + ")"); }); this.timer = setInterval(() => { this.cycleBackground(); @@ -206,7 +206,7 @@ export class LoginComponent implements OnDestroy, 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 + ")"); + .bypassSecurityTrustStyle("url(" + x.url + ")"); }); } } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/artist/artist-details.component.html b/src/Ombi/ClientApp/src/app/media-details/components/artist/artist-details.component.html index 46c5b97c9..1a7db7f91 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/artist/artist-details.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/artist/artist-details.component.html @@ -23,12 +23,12 @@
+ {{ 'MediaDetails.RequestAllAlbums' | translate }} + {{ 'MediaDetails.RequestSelectedAlbums' | translate }} + {{ 'MediaDetails.ClearSelection' | translate }} @@ -39,39 +39,39 @@ --> diff --git a/src/Ombi/ClientApp/src/app/media-details/components/index.ts b/src/Ombi/ClientApp/src/app/media-details/components/index.ts index 001eb20bf..240fba28a 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/index.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/index.ts @@ -9,7 +9,6 @@ import { MediaPosterComponent } from "./shared/media-poster/media-poster.compone 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"; import { SearchService, RequestService, RadarrService, IssuesService, SonarrService } from "../../services"; import { RequestServiceV2 } from "../../services/requestV2.service"; @@ -18,7 +17,9 @@ import { ArtistDetailsComponent } from "./artist/artist-details.component"; import { ArtistInformationPanel } from "./artist/panels/artist-information-panel/artist-information-panel.component"; import { ArtistReleasePanel } from "./artist/panels/artist-release-panel/artist-release-panel.component"; import { IssuesPanelComponent } from "./shared/issues-panel/issues-panel.component"; -import { TvAdminPanelComponent } from "./tv/panels/tv-admin-panel/tv-admin-panel.component"; +import { TvAdvancedOptionsComponent } from "./tv/panels/tv-advanced-options/tv-advanced-options.component"; +import { RequestBehalfComponent } from "./shared/request-behalf/request-behalf.component"; +import { TvRequestGridComponent } from "./tv/panels/tv-request-grid/tv-request-grid.component"; export const components: any[] = [ MovieDetailsComponent, @@ -32,21 +33,24 @@ export const components: any[] = [ CastCarouselComponent, DenyDialogComponent, TvRequestsPanelComponent, - MovieAdminPanelComponent, MovieAdvancedOptionsComponent, + TvAdvancedOptionsComponent, NewIssueComponent, ArtistDetailsComponent, ArtistInformationPanel, ArtistReleasePanel, + RequestBehalfComponent, IssuesPanelComponent, - TvAdminPanelComponent, + TvRequestGridComponent, ]; export const entryComponents: any[] = [ YoutubeTrailerComponent, DenyDialogComponent, MovieAdvancedOptionsComponent, + TvAdvancedOptionsComponent, NewIssueComponent, + RequestBehalfComponent, ]; export const providers: any[] = [ 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 index 375ca40ec..ffa06a502 100644 --- 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 @@ -5,84 +5,106 @@
- +
-
- - - - -
- - - +
+
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+
+ + + {{'Search.ViewOnPlex' | translate}} + + + + {{'Search.ViewOnEmby' | translate}} + + + + {{'Search.ViewOnJellyfin' | translate}} + + + + + + + + + + + + + + + + + + + + + + + +
-
- - - - - - - - + @@ -107,6 +129,28 @@
+ + +
+
+ + Trailers + + + + + + + + +
+
+
@@ -131,7 +175,6 @@
-
@@ -152,50 +195,16 @@
- -
-
- - - - - {{'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 index 55a69fe15..988859245 100644 --- 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 @@ -1,5 +1,5 @@ -import { Component, ViewEncapsulation } from "@angular/core"; -import { ImageService, SearchV2Service, RequestService, MessageService } from "../../../services"; +import { AfterViewInit, Component, ViewChild, ViewEncapsulation } from "@angular/core"; +import { ImageService, SearchV2Service, RequestService, MessageService, RadarrService } from "../../../services"; import { ActivatedRoute } from "@angular/router"; import { DomSanitizer } from "@angular/platform-browser"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; @@ -9,7 +9,10 @@ import { AuthService } from "../../../auth/auth.service"; import { IMovieRequests, RequestType, IAdvancedData } from "../../../interfaces"; import { DenyDialogComponent } from "../shared/deny-dialog/deny-dialog.component"; import { NewIssueComponent } from "../shared/new-issue/new-issue.component"; -import { StorageService } from "../../../shared/storage/storage-service"; +import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component"; +import { RequestServiceV2 } from "../../../services/requestV2.service"; +import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component"; +import { forkJoin } from "rxjs"; @Component({ templateUrl: "./movie-details.component.html", @@ -24,22 +27,25 @@ export class MovieDetailsComponent { public advancedOptions: IAdvancedData; public showAdvanced: boolean; // Set on the UI + public requestType = RequestType.movie; + + private theMovidDbId: number; private imdbId: string; 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, - private storage: StorageService) { - this.route.params.subscribe((params: any) => { + private requestService2: RequestServiceV2, private radarrService: RadarrService, + public messageService: MessageService, private auth: AuthService) { + this.route.params.subscribe(async (params: any) => { if (typeof params.movieDbId === 'string' || params.movieDbId instanceof String) { if (params.movieDbId.startsWith("tt")) { this.imdbId = params.movieDbId; } } this.theMovidDbId = params.movieDbId; - this.load(); + await this.load(); }); } @@ -47,6 +53,10 @@ export class MovieDetailsComponent { this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + if (this.isAdmin) { + this.showAdvanced = await this.radarrService.isRadarrEnabled(); + } + if (this.imdbId) { this.searchService.getMovieByImdbId(this.imdbId).subscribe(async x => { this.movie = x; @@ -55,10 +65,7 @@ export class MovieDetailsComponent { 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 + ")"); - }); + this.loadBanner(); }); } else { this.searchService.getFullMovieDetails(this.theMovidDbId).subscribe(async x => { @@ -67,17 +74,15 @@ export class MovieDetailsComponent { // Load up this request this.hasRequest = true; this.movieRequest = await this.requestService.getMovieRequest(this.movie.requestId); + this.loadAdvancedInfo(); } - this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => { - this.movie.background = this.sanitizer.bypassSecurityTrustStyle - ("url(" + x + ")"); - }); + this.loadBanner(); }); } } - public async request() { - const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null }).toPromise(); + public async request(userId?: string) { + const result = await this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, languageCode: null, requestOnBehalf: userId }).toPromise(); if (result.result) { this.movie.requested = true; this.messageService.send(result.message, "Ok"); @@ -138,10 +143,69 @@ export class MovieDetailsComponent { public setAdvancedOptions(data: IAdvancedData) { this.advancedOptions = data; if (data.rootFolderId) { - this.movieRequest.qualityOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path; + this.movieRequest.qualityOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name; } if (data.profileId) { - this.movieRequest.rootPathOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name; + this.movieRequest.rootPathOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path; } } + + public async openAdvancedOptions() { + const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: { movieRequest: this.movieRequest }, panelClass: 'modal-panel' }) + await dialog.afterClosed().subscribe(async result => { + if (result) { + result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0]; + result.profile = result.profiles.filter(f => f.id === +result.profileId)[0]; + await this.requestService2.updateMovieAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.movieRequest.id }).toPromise(); + this.setAdvancedOptions(result); + } + }); + } + + public async openRequestOnBehalf() { + const dialog = this.dialog.open(RequestBehalfComponent, { width: "700px", panelClass: 'modal-panel' }) + await dialog.afterClosed().subscribe(async result => { + if (result) { + await this.request(result.id); + } + }); + } + + private loadBanner() { + this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => { + if (!this.movie.backdropPath) { + this.movie.background = this.sanitizer.bypassSecurityTrustStyle + ("url(" + x + ")"); + } else { + this.movie.background = this.sanitizer.bypassSecurityTrustStyle + ("url(https://image.tmdb.org/t/p/original/" + this.movie.backdropPath + ")"); + } + }); + } + + private loadAdvancedInfo() { + const profile = this.radarrService.getQualityProfilesFromSettings(); + const folders = this.radarrService.getRootFoldersFromSettings(); + + forkJoin([profile, folders]).subscribe(x => { + debugger; + const radarrProfiles = x[0]; + const radarrRootFolders = x[1]; + + const profile = radarrProfiles.filter((p) => { + return p.id === this.movieRequest.qualityOverride; + }); + if (profile.length > 0) { + this.movieRequest.qualityOverrideTitle = profile[0].name; + } + + const path = radarrRootFolders.filter((folder) => { + return folder.id === this.movieRequest.rootPathOverride; + }); + if (path.length > 0) { + this.movieRequest.rootPathOverrideTitle = path[0].path; + } + + }); + } } 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 deleted file mode 100644 index 94bb66a12..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ 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 deleted file mode 100644 index e19840a6d..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component, Input, OnInit, EventEmitter, Output } from "@angular/core"; -import { RadarrService } from "../../../../../services"; -import { IRadarrProfile, IRadarrRootFolder, IMovieRequests, IAdvancedData } from "../../../../../interfaces"; -import { MatDialog } from "@angular/material/dialog"; -import { MovieAdvancedOptionsComponent } from "../movie-advanced-options/movie-advanced-options.component"; -import { RequestServiceV2 } from "../../../../../services/requestV2.service"; - -@Component({ - templateUrl: "./movie-admin-panel.component.html", - selector: "movie-admin-panel", -}) -export class MovieAdminPanelComponent implements OnInit { - - @Input() public movie: IMovieRequests; - @Output() public advancedOptionsChanged = new EventEmitter(); - @Output() public radarrEnabledChange = new EventEmitter(); - - public radarrEnabled: boolean; - public radarrProfiles: IRadarrProfile[]; - public selectedRadarrProfile: IRadarrProfile; - public radarrRootFolders: IRadarrRootFolder[]; - public selectRadarrRootFolders: IRadarrRootFolder; - - constructor(private radarrService: RadarrService, private requestService: RequestServiceV2, private dialog: MatDialog) { } - - public async ngOnInit() { - this.radarrEnabled = await this.radarrService.isRadarrEnabled(); - if (this.radarrEnabled) { - this.radarrService.getQualityProfilesFromSettings().subscribe(c => { - this.radarrProfiles = c; - this.setQualityOverrides(); - }); - this.radarrService.getRootFoldersFromSettings().subscribe(c => { - this.radarrRootFolders = c; - this.setRootFolderOverrides(); - }); - } - - this.radarrEnabledChange.emit(this.radarrEnabled); - } - - public async openAdvancedOptions() { - const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: { profiles: this.radarrProfiles, rootFolders: this.radarrRootFolders }, panelClass: 'modal-panel' }) - await dialog.afterClosed().subscribe(async result => { - if(result) { - // get the name and ids - result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0]; - result.profile = result.profiles.filter(f => f.id === +result.profileId)[0]; - await this.requestService.updateMovieAdvancedOptions({qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.movie.id}).toPromise(); - this.advancedOptionsChanged.emit(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 index 8caecdfdf..e839b49b1 100644 --- 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 @@ -5,7 +5,7 @@ {{'MediaDetails.QualityProfilesSelect' | translate }} - {{profile.name}} + {{profile.name}}
@@ -13,7 +13,7 @@ {{'MediaDetails.RootFolderSelect' | translate }} - {{profile.path}} + {{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 index d5c3310fb..d164644ff 100644 --- 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 @@ -1,14 +1,55 @@ -import { Component, Inject } from "@angular/core"; +import { Component, Inject, OnInit } from "@angular/core"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; -import { IAdvancedData } from "../../../../../interfaces"; +import { IAdvancedData, IRadarrProfile, IRadarrRootFolder } from "../../../../../interfaces"; +import { RadarrService } from "../../../../../services"; @Component({ templateUrl: "./movie-advanced-options.component.html", selector: "movie-advanced-options", }) -export class MovieAdvancedOptionsComponent { - +export class MovieAdvancedOptionsComponent implements OnInit { + + public radarrProfiles: IRadarrProfile[]; + public radarrRootFolders: IRadarrRootFolder[]; + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData, - ) { - } + private radarrService: RadarrService + ) { + } + + + public async ngOnInit() { + this.radarrService.getQualityProfilesFromSettings().subscribe(c => { + this.radarrProfiles = c; + this.data.profiles = c; + this.setQualityOverrides(); + }); + this.radarrService.getRootFoldersFromSettings().subscribe(c => { + this.radarrRootFolders = c; + this.data.rootFolders = c; + this.setRootFolderOverrides(); + }); + } + + private setQualityOverrides(): void { + if (this.radarrProfiles) { + const profile = this.radarrProfiles.filter((p) => { + return p.id === this.data.movieRequest.qualityOverride; + }); + if (profile.length > 0) { + this.data.movieRequest.qualityOverrideTitle = profile[0].name; + } + } + } + + private setRootFolderOverrides(): void { + if (this.radarrRootFolders) { + const path = this.radarrRootFolders.filter((folder) => { + return folder.id === this.data.movieRequest.rootPathOverride; + }); + if (path.length > 0) { + this.data.movieRequest.rootPathOverrideTitle = path[0].path; + } + } + } } 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 index 449dd09c4..b14e2d44a 100644 --- 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 @@ -1,92 +1,120 @@ -
-
- {{'MediaDetails.Status' | translate }}: -
{{movie.status}}
+
+
+ + {{movie.voteAverage | number:'1.0-1'}}/10 + + + + {{ratings.critics_score}}% + + + + {{ratings.audience_score}}% +
+
+
+ {{'MediaDetails.StreamingOn' | translate }}: +
+ + + +
+
+
- {{'MediaDetails.Availability' | translate }} -
{{'Common.Available' | translate}}
-
{{'Common.NotAvailable' | translate}}
- + {{'MediaDetails.Status' | translate }}: + {{movie.status}}
-
- {{'MediaDetails.RequestStatus' | translate }} + {{'MediaDetails.Availability' | translate }}: + {{'Common.Available' | translate}} + {{'Common.NotAvailable' | translate}} +
+
+ {{'MediaDetails.RequestStatus' | translate }}
{{'Common.ProcessingRequest' | translate}}
{{'Common.PendingApproval' | translate}}
{{'Common.NotRequested' | translate}}
+ +
+ {{'Requests.RequestedBy' | translate }}: + {{request.requestedUser.userAlias}} +
+ +
+ {{'Requests.RequestDate' | translate }}: + {{request.requestedDate | date}} +
+ +
- {{'MediaDetails.Quality' | translate }}: + {{'MediaDetails.Quality' | translate }}:
{{movie.quality | quality}}
-
- {{'MediaDetails.RootFolderOverride' | translate }} +
+ {{'MediaDetails.RootFolderOverride' | translate }}
{{request.rootPathOverrideTitle}}
-
- {{'MediaDetails.QualityOverride' | translate }} +
+ {{'MediaDetails.QualityOverride' | translate }}
{{request.qualityOverrideTitle}}
-
-
- {{'MediaDetails.Genres' | translate }}: -
- - - {{genre.name}} - - -
-
-
- {{'MediaDetails.TheatricalRelease' | translate }}: -
- {{movie.releaseDate | date: 'mediumDate'}} -
- {{'MediaDetails.DigitalRelease' | translate }}: -
- {{movie.digitalReleaseDate | date: 'mediumDate'}} -
-
-
- {{'MediaDetails.UserScore' | translate }}: -
- {{movie.voteAverage | number:'1.0-1'}} / 10 -
-
-
- {{'MediaDetails.Votes' | translate }}: -
- {{movie.voteCount | thousandShort: 1}} -
-
-
- {{'MediaDetails.Runtime' | translate }}: -
{{'MediaDetails.Minutes' | translate:{runtime: movie.runtime} }}
-
-
- {{'MediaDetails.Revenue' | translate }}: -
{{movie.revenue | currency: 'USD'}}
-
-
- {{'MediaDetails.Budget' | translate }}: -
{{movie.budget | currency: 'USD'}}
-
+
+ + {{'MediaDetails.TheatricalRelease' | translate }}: + {{movie.releaseDate | date: 'mediumDate'}} + +
+ {{'MediaDetails.DigitalRelease' | translate }}: + {{movie.digitalReleaseDate | date: 'mediumDate'}} +
+ +
+ {{'MediaDetails.Votes' | translate }}: + {{movie.voteCount | thousandShort: 1}} +
+
+ {{'MediaDetails.Runtime' | translate }}: + {{'MediaDetails.Minutes' | translate:{runtime: movie.runtime} }} +
+
+ {{'MediaDetails.Revenue' | translate }}: + {{movie.revenue | currency: 'USD'}} +
+
+ {{'MediaDetails.Budget' | translate }}: + {{movie.budget | currency: 'USD'}} +
-
+
+
+ {{'MediaDetails.Genres' | translate }}:
- {{'MediaDetails.Keywords' | translate }}: - - {{keyword.name}} + + {{genre.name}}
+
+ +
+
+ {{'MediaDetails.Keywords' | translate }}: + + + {{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 index 3bfb68f56..89d9c076c 100644 --- 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 @@ -1,15 +1,31 @@ -import { Component, ViewEncapsulation, Input } from "@angular/core"; +import { Component, ViewEncapsulation, Input, OnInit, Inject } from "@angular/core"; import { ISearchMovieResultV2 } from "../../../../interfaces/ISearchMovieResultV2"; -import { IAdvancedData, IMovieRequests } from "../../../../interfaces"; - +import { IMovieRequests } from "../../../../interfaces"; +import { SearchV2Service } from "../../../../services/searchV2.service"; +import { IMovieRatings } from "../../../../interfaces/IRatings"; +import { APP_BASE_HREF } from "@angular/common"; +import { IStreamingData } from "../../../../interfaces/IStreams"; @Component({ templateUrl: "./movie-information-panel.component.html", styleUrls: ["../../../media-details.component.scss"], selector: "movie-information-panel", encapsulation: ViewEncapsulation.None }) -export class MovieInformationPanelComponent { +export class MovieInformationPanelComponent implements OnInit { + + constructor(private searchService: SearchV2Service, @Inject(APP_BASE_HREF) public baseUrl: string) { } + @Input() public movie: ISearchMovieResultV2; @Input() public request: IMovieRequests; @Input() public advancedOptions: boolean; + + public ratings: IMovieRatings; + public streams: IStreamingData[]; + + public ngOnInit() { + this.searchService.getRottenMovieRatings(this.movie.title, +this.movie.releaseDate.toString().substring(0,4)) + .subscribe(x => this.ratings = x); + + this.searchService.getMovieStreams(this.movie.id).subscribe(x => this.streams = x); + } } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/request-behalf/request-behalf.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/request-behalf/request-behalf.component.html new file mode 100644 index 000000000..db7acde9d --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/request-behalf/request-behalf.component.html @@ -0,0 +1,22 @@ +

{{ 'MediaDetails.RequestOnBehalf' | translate}}

+
+ + + + + + {{option.username}} + + + + +
+
+ + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/request-behalf/request-behalf.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/request-behalf/request-behalf.component.ts new file mode 100644 index 000000000..da684db06 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/request-behalf/request-behalf.component.ts @@ -0,0 +1,52 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { IDenyDialogData } from "../interfaces/interfaces"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { RequestService, MessageService, IdentityService } from "../../../../services"; +import { RequestType, IRequestEngineResult, IUserDropdown } from "../../../../interfaces"; +import { FormControl } from "@angular/forms"; +import { Observable } from "rxjs"; +import { filter, map, startWith } from "rxjs/operators"; + + + +@Component({ + selector: "request-behalf", + templateUrl: "./request-behalf.component.html", +}) +export class RequestBehalfComponent implements OnInit { + constructor( + public dialogRef: MatDialogRef, + public identity: IdentityService) { } + + public myControl = new FormControl(); + public options: IUserDropdown[]; + public filteredOptions: Observable; + public userId: string; + + public async ngOnInit() { + this.options = await this.identity.getUsersDropdown().toPromise(); + this.filteredOptions = this.myControl.valueChanges + .pipe( + startWith(''), + map(value => this._filter(value)) + ); + } + + public request() { + this.dialogRef.close(this.myControl.value); + } + + public onNoClick(): void { + this.dialogRef.close(); + } + + public displayFn(user: IUserDropdown): string { + return user?.username ? user.username : ''; + } + + private _filter(value: string|IUserDropdown): IUserDropdown[] { + const filterValue = typeof value === 'string' ? value.toLowerCase() : value.username.toLowerCase(); + + return this.options.filter(option => option.username.toLowerCase().includes(filterValue)); + } +} 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 index f5772842b..c49b52e1c 100644 --- 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 @@ -1,43 +1,42 @@ - - + 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 index 866723b93..1da3e6841 100644 --- 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 @@ -1,9 +1,10 @@ +@import "~styles/variables.scss"; - -.media-icons.plex { - color: #feb801 !important; +a.media-icons:hover{ + color:$ombi-active; } -.media-icons.emby { - color: #52b54a !important; +button.admin-cog{ + margin-left:40px; + color:$ombi-active; } \ No newline at end of file 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 index 522efb6b8..715320ff2 100644 --- 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 @@ -1,12 +1,11 @@ -import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; - +import { Component, Input, Output, EventEmitter } from "@angular/core"; +import { RequestType } from "../../../../interfaces"; @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; @@ -18,12 +17,30 @@ export class SocialIconsComponent { @Input() available: boolean; @Input() plexUrl: string; @Input() embyUrl: string; + @Input() jellyfinUrl: string; @Input() doNotAppend: boolean; - + @Input() type: RequestType; + + @Input() isAdmin: boolean; + @Input() canRequestOnBehalf: boolean; + @Input() canShowAdvanced: boolean; + @Output() openTrailer: EventEmitter = new EventEmitter(); - + @Output() onRequestBehalf: EventEmitter = new EventEmitter(); + @Output() onAdvancedOptions: EventEmitter = new EventEmitter(); + + public RequestType = RequestType; + public openDialog() { this.openTrailer.emit(); } + + public openRequestOnBehalf() { + this.onRequestBehalf.emit(); + } + + public openAdvancedOptions() { + this.onAdvancedOptions.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 index 7d88fa63f..570db9b9c 100644 --- 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 @@ -1,18 +1,19 @@
-
+
+
-
+
-

{{title}} + class="mobile-top-text"> +

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

-
{{tagline}}
+

{{tagline}}

diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.scss b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.scss new file mode 100644 index 000000000..972d78d69 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.scss @@ -0,0 +1,21 @@ +.large-text { + font: 500 40px/30px Roboto, "Helvetica Neue", sans-serif; +} + +@media (min-width:571px){ + .title-top-banner{ + padding-left:330px; + margin-left:0px; +}} + +@media (min-width:2400px){ + .title-top-banner{ + padding-left:390px; + margin-left:0px; +}} + +@media (max-width:571px){ + .title-top-banner{ + text-align:center; + top:50px; +}} \ 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 index 583376537..06187baf1 100644 --- 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 @@ -4,17 +4,17 @@ import { DomSanitizer, SafeStyle } from "@angular/platform-browser"; @Component({ selector: "top-banner", templateUrl: "./top-banner.component.html", + styleUrls: ["top-banner.component.scss"] }) export class TopBannerComponent { - @Input() title: string; @Input() releaseDate: Date; @Input() tagline: string; @Input() available: boolean; @Input() background: any; - - constructor(private sanitizer:DomSanitizer){} + + constructor(private sanitizer:DomSanitizer){ } public getBackgroundImage(): SafeStyle { return this.sanitizer.bypassSecurityTrustStyle(this.background); diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-admin-panel/tv-admin-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-admin-panel/tv-admin-panel.component.html deleted file mode 100644 index 473f0e97b..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-admin-panel/tv-admin-panel.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-admin-panel/tv-admin-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-admin-panel/tv-admin-panel.component.ts deleted file mode 100644 index 40ca64e1c..000000000 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-admin-panel/tv-admin-panel.component.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Component, Input, OnInit, EventEmitter, Output } from "@angular/core"; -import { RadarrService, SonarrService } from "../../../../../services"; -import { IRadarrProfile, IRadarrRootFolder, IAdvancedData, ITvRequests, ISonarrProfile, ISonarrRootFolder } from "../../../../../interfaces"; -import { MatDialog } from "@angular/material/dialog"; - -import { RequestServiceV2 } from "../../../../../services/requestV2.service"; -import { MovieAdvancedOptionsComponent } from "../../../movie/panels/movie-advanced-options/movie-advanced-options.component"; - -@Component({ - templateUrl: "./tv-admin-panel.component.html", - selector: "tv-admin-panel", -}) -export class TvAdminPanelComponent implements OnInit { - - @Input() public tv: ITvRequests; - @Output() public advancedOptionsChanged = new EventEmitter(); - @Output() public sonarrEnabledChange = new EventEmitter(); - - public sonarrEnabled: boolean; - public radarrProfiles: IRadarrProfile[]; - public selectedRadarrProfile: IRadarrProfile; - public radarrRootFolders: IRadarrRootFolder[]; - public selectRadarrRootFolders: IRadarrRootFolder; - - - public sonarrProfiles: ISonarrProfile[]; - public sonarrRootFolders: ISonarrRootFolder[]; - - constructor(private sonarrService: SonarrService, private requestService: RequestServiceV2, private dialog: MatDialog) { } - - public async ngOnInit() { - this.sonarrEnabled = await this.sonarrService.isEnabled(); - if (this.sonarrEnabled) { - this.sonarrService.getQualityProfilesWithoutSettings() - .subscribe(x => { - this.sonarrProfiles = x; - this.setQualityOverrides(); - }); - this.sonarrService.getRootFoldersWithoutSettings() - .subscribe(x => { - this.sonarrRootFolders = x; - this.setRootFolderOverrides(); - }); - } - - this.sonarrEnabledChange.emit(this.sonarrEnabled); - } - - public async openAdvancedOptions() { - const dialog = this.dialog.open(MovieAdvancedOptionsComponent, { width: "700px", data: { profiles: this.sonarrProfiles, rootFolders: this.sonarrRootFolders }, panelClass: 'modal-panel' }) - await dialog.afterClosed().subscribe(async result => { - if (result) { - // get the name and ids - result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0]; - result.profile = result.profiles.filter(f => f.id === +result.profileId)[0]; - await this.requestService.updateTvAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.tv.id }).toPromise(); - this.advancedOptionsChanged.emit(result); - } - }); - } - - private setQualityOverrides(): void { - if (this.sonarrProfiles) { - const profile = this.sonarrProfiles.filter((p) => { - return p.id === this.tv.qualityOverride; - }); - if (profile.length > 0) { - this.tv.qualityOverrideTitle = profile[0].name; - } - } - } - - private setRootFolderOverrides(): void { - if (this.sonarrRootFolders) { - const path = this.sonarrRootFolders.filter((folder) => { - return folder.id === this.tv.rootFolder; - }); - if (path.length > 0) { - this.tv.rootPathOverrideTitle = path[0].path; - } - } - } -} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-advanced-options/tv-advanced-options.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-advanced-options/tv-advanced-options.component.html new file mode 100644 index 000000000..5724a35bf --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-advanced-options/tv-advanced-options.component.html @@ -0,0 +1,23 @@ +

+ + Advanced Options

+
+ + {{'MediaDetails.QualityProfilesSelect' | translate }} + + {{profile.name}} + + +
+
+ + {{'MediaDetails.RootFolderSelect' | translate }} + + {{profile.path}} + + +
+
+ + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-advanced-options/tv-advanced-options.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-advanced-options/tv-advanced-options.component.ts new file mode 100644 index 000000000..7846b69df --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-advanced-options/tv-advanced-options.component.ts @@ -0,0 +1,55 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { IAdvancedData, ISonarrProfile, ISonarrRootFolder } from "../../../../../interfaces"; +import { SonarrService } from "../../../../../services"; + +@Component({ + templateUrl: "./tv-advanced-options.component.html", + selector: "tv-advanced-options", +}) +export class TvAdvancedOptionsComponent implements OnInit { + + public sonarrProfiles: ISonarrProfile[]; + public sonarrRootFolders: ISonarrRootFolder[]; + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData, + private sonarrService: SonarrService + ) { + } + + + public async ngOnInit() { + this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => { + this.sonarrProfiles = c; + this.data.profiles = c; + this.setQualityOverrides(); + }); + this.sonarrService.getRootFoldersWithoutSettings().subscribe(c => { + this.sonarrRootFolders = c; + this.data.rootFolders = c; + this.setRootFolderOverrides(); + }); + } + + private setQualityOverrides(): void { + if (this.sonarrProfiles) { + const profile = this.sonarrProfiles.filter((p) => { + return p.id === this.data.tvRequest.qualityOverride; + }); + if (profile.length > 0) { + this.data.movieRequest.qualityOverrideTitle = profile[0].name; + } + } + } + + private setRootFolderOverrides(): void { + if (this.sonarrRootFolders) { + const path = this.sonarrRootFolders.filter((folder) => { + return folder.id === this.data.tvRequest.rootFolder; + }); + if (path.length > 0) { + this.data.movieRequest.rootPathOverrideTitle = path[0].path; + } + } + } +} 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 index fb3c1c65e..777f4820e 100644 --- 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 @@ -1,42 +1,55 @@
+ + {{tv.rating}}/10 + + + {{ratings.score}}% + + +
+
+ {{'MediaDetails.StreamingOn' | translate }}: +
+ + + +
+
+
{{'MediaDetails.Status' | translate }}: -
{{tv.status}} -
First Aired: -
{{tv.firstAired | date: 'mediumDate'}} -
-
- {{'MediaDetails.RootFolderOverride' | translate }} +
+ Seasons: + {{seasonCount}} +
+
+ Episodes: + {{totalEpisodes}} +
+ +
+ {{'MediaDetails.RootFolderOverride' | translate }}:
{{request.rootPathOverrideTitle}}
-
- {{'MediaDetails.QualityOverride' | translate }} +
+ {{'MediaDetails.QualityOverride' | translate }}:
{{request.qualityOverrideTitle}}
{{'MediaDetails.Runtime' | translate }}: -
{{'MediaDetails.Minutes' | translate:{ runtime: tv.runtime} }} -
-
- Rating: -
- {{tv.rating}} / 10 -
-
+
Network: -
{{tv.network.name}} -
@@ -48,16 +61,3 @@
-
-
- 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 index 3d3f77e75..350f76c26 100644 --- 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 @@ -1,6 +1,9 @@ import { Component, ViewEncapsulation, Input, OnInit } from "@angular/core"; import { ITvRequests } from "../../../../../interfaces"; +import { ITvRatings } from "../../../../../interfaces/IRatings"; import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2"; +import { IStreamingData } from "../../../../../interfaces/IStreams"; +import { SearchV2Service } from "../../../../../services"; @Component({ templateUrl: "./tv-information-panel.component.html", @@ -9,15 +12,24 @@ import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2"; encapsulation: ViewEncapsulation.None }) export class TvInformationPanelComponent implements OnInit { + + constructor(private searchService: SearchV2Service) { } + @Input() public tv: ISearchTvResultV2; @Input() public request: ITvRequests; @Input() public advancedOptions: boolean; + public ratings: ITvRatings; + public streams: IStreamingData[]; public seasonCount: number; public totalEpisodes: number = 0; public nextEpisode: any; public ngOnInit(): void { + this.searchService.getRottenTvRatings(this.tv.title, +this.tv.firstAired.toString().substring(0,4)) + .subscribe(x => this.ratings = x); + + this.searchService.getTvStreams(+this.tv.theTvDbId, this.tv.id).subscribe(x => this.streams = x); this.tv.seasonRequests.forEach(season => { this.totalEpisodes = this.totalEpisodes + season.episodes.length; }); diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html new file mode 100644 index 000000000..91e4b2d8f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html @@ -0,0 +1,81 @@ + + + + + +
+ {{ 'Requests.Season' | translate }} {{season.seasonNumber}} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + # {{element.episodeNumber}} {{ 'Requests.GridTitle' | translate }} {{element.title}} {{ 'Requests.AirDate' | translate }} {{element.airDate | amLocal | amDateFormat: 'L' }} {{ 'Requests.GridStatus' | translate }} +
+ {{ep.requestStatus | translate}} +
+
+
+ +
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.scss b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.scss new file mode 100644 index 000000000..d96f849d8 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.scss @@ -0,0 +1,44 @@ +.floating-fab { + position: fixed; + right: 3%; + bottom: 6%; + z-index: 10; +} + + +.top-right span:before{ + content: ''; + width: 10px; + height: 10px; + -moz-border-radius: 7.5px; + -webkit-border-radius: 7.5px; + border-radius: 7.5px; + margin-right:5px; + } + + .top-right.available span{ + display:block; + } + + .top-right.available span:before{ + display: inline-block; + background-color: #1DE9B6; + } + + .top-right.approved span { + display: block; + } + + .top-right.approved span:before{ + display: inline-block; + background-color: #ff5722; + } + + .top-right.requested span { + display: block; + } + + .top-right.requested span:before{ + display: inline-block; + background-color: #ffd740; + } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.ts new file mode 100644 index 000000000..b6b57f1dd --- /dev/null +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.ts @@ -0,0 +1,215 @@ +import { Component, Input } from "@angular/core"; +import { IChildRequests, IEpisodesRequests, INewSeasonRequests, ISeasonsViewModel, ITvRequestViewModel, RequestType } from "../../../../../interfaces"; +import { RequestService } from "../../../../../services/request.service"; +import { MessageService } from "../../../../../services"; +import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component"; +import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2"; +import { MatDialog } from "@angular/material/dialog"; +import { SelectionModel } from "@angular/cdk/collections"; + +@Component({ + templateUrl: "./tv-request-grid.component.html", + styleUrls: ["./tv-request-grid.component.scss"], + selector: "tv-request-grid" +}) +export class TvRequestGridComponent { + @Input() public tv: ISearchTvResultV2; + @Input() public tvRequest: IChildRequests[]; + @Input() public isAdmin: boolean; + public selection = new SelectionModel(true, []); + + public get requestable() { + return this.tv?.seasonRequests?.length > 0 + } + + public displayedColumns: string[] = ['select', 'number', 'title', 'airDate', 'status']; + + constructor(private requestService: RequestService, private notificationService: MessageService, + private dialog: MatDialog) { + + } + + public async submitRequests() { + // Make sure something has been selected + const selected = this.selection.hasValue(); + if (!selected && !this.tv.requestAll && !this.tv.firstSeason && !this.tv.latestSeason) { + this.notificationService.send("You need to select some episodes!", "OK"); + return; + } + + this.tv.requested = true; + + const viewModel = { + firstSeason: this.tv.firstSeason, latestSeason: this.tv.latestSeason, requestAll: this.tv.requestAll, tvDbId: this.tv.id, + requestOnBehalf: null + }; + viewModel.seasons = []; + this.tv.seasonRequests.forEach((season) => { + const seasonsViewModel = { seasonNumber: season.seasonNumber, episodes: [] }; + if (!this.tv.latestSeason && !this.tv.requestAll && !this.tv.firstSeason) { + season.episodes.forEach(ep => { + if (this.selection.isSelected(ep)) { + 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.tv.title} has been added successfully`); + + this.selection.clear(); + + } else { + this.notificationService.send(requestResult.errorMessage ? requestResult.errorMessage : requestResult.message); + } + } + + 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.notificationService.send("Request has been approved", "Ok"); + } else { + this.notificationService.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.notificationService.send( + `This request is now available`); + } else { + this.notificationService.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.notificationService.send( + `This request is now unavailable`); + } else { + this.notificationService.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; + }); + }); + }); + } + + public async requestAllSeasons() { + this.tv.requestAll = true; + await this.submitRequests(); + } + + public async requestFirstSeason() { + this.tv.firstSeason = true; + await this.submitRequests(); + } + + public async requestLatestSeason() { + this.tv.latestSeason = true; + await this.submitRequests(); + } + + /** Whether the number of selected elements matches the total number of rows. */ + public isAllSelected(dataSource: IEpisodesRequests[]) { + const numSelected = this.selection.selected.length; + const numRows = dataSource.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + public masterToggle(dataSource: IEpisodesRequests[]) { + this.isAllSelected(dataSource) ? + this.selection.clear() : + dataSource.forEach(row => { + if (!row.available && !row.requested && !row.approved) { + this.selection.select(row) + } + }); + } + + public isSeasonCheckable(season: INewSeasonRequests) { + const seasonAvailable = season.episodes.every((ep) => { + return ep.available || ep.requested || ep.approved; + }); + return !seasonAvailable; + } + + public getStatusClass(season: INewSeasonRequests): string { + const seasonAvailable = season.episodes.every((ep) => { + return ep.available; + }); + if (seasonAvailable) { + return "available"; + } + + const seasonPending = season.episodes.some((ep) => { + return ep.requested && !ep.approved + }); + if (seasonPending) { + return "requested"; + } + + const seasonApproved = season.episodes.some((ep) => { + return ep.requested && ep.approved + }); + if (seasonApproved) { + return "approved"; + } + return ""; + } + + public getEpisodeStatusClass(ep: IEpisodesRequests): string { + if (ep.available) { + return "available"; + } + + if (ep.requested && !ep.approved) { + return "requested"; + } + + if (ep.requested && ep.approved) { + return "approved"; + } + return ""; + } +} 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 index 661c11e86..c8588b1c7 100644 --- 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 @@ -15,7 +15,7 @@
- Requested By '{{request.requestedUser.userAlias}}' on + {{'Requests.RequestedBy' | translate}} '{{request.requestedUser.userAlias}}' on {{request.requestedDate | amLocal | amDateFormat: 'LL' }} - {{request.deniedReason}} @@ -29,7 +29,7 @@ - {{ 'Requests.Number' | translate }} + # {{element.episodeNumber}} @@ -50,7 +50,7 @@ -
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 index 0df2ba1fb..976fcfefc 100644 --- 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 @@ -2,127 +2,144 @@
-
-
-

{{ 'MediaDetails.NotEnoughInfo' | translate }}

+
+
+

+

{{ 'MediaDetails.NotEnoughInfo' | translate }}

+
-
- - - - -
- - -
-
-
- - - - -
- - - - -
-
- - - - - - - - -
+ + +
+ + + -
-
- - - - - - - - - - - - - -
- +
+
+
+
+ +
+ +
+
+ + + + + + + +
+
+
-
-
- - - {{tv.overview}} +
+ + +
-
- +
+
+
+ + + {{tv.overview}} + + +
+
+ +
+
-
-
-
-
-
+
+
- + +
-
+
+ +
+
-
-
- -
- - - - - Requests - - - - - - + -
+
+
+
+ +
+ + + + + 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 index ef86fbdce..a5f80d2cf 100644 --- 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 @@ -5,10 +5,14 @@ import { DomSanitizer } from "@angular/platform-browser"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { MatDialog } from "@angular/material/dialog"; import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component"; -import { EpisodeRequestComponent } from "../../../shared/episode-request/episode-request.component"; -import { IAdvancedData, IChildRequests, ISonarrProfile, ISonarrRootFolder, ITvRequests, RequestType } from "../../../interfaces"; +import { EpisodeRequestComponent, EpisodeRequestData } from "../../../shared/episode-request/episode-request.component"; +import { IAdvancedData, IChildRequests, ITvRequests, RequestType } from "../../../interfaces"; import { AuthService } from "../../../auth/auth.service"; import { NewIssueComponent } from "../shared/new-issue/new-issue.component"; +import { TvAdvancedOptionsComponent } from "./panels/tv-advanced-options/tv-advanced-options.component"; +import { RequestServiceV2 } from "../../../services/requestV2.service"; +import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component"; +import { forkJoin } from "rxjs"; @Component({ templateUrl: "./tv-details.component.html", @@ -24,12 +28,14 @@ export class TvDetailsComponent implements OnInit { public isAdmin: boolean; public advancedOptions: IAdvancedData; public showAdvanced: boolean; // Set on the UI + public requestType = RequestType.tvShow; 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 requestService2: RequestServiceV2, private auth: AuthService, private sonarrService: SonarrService) { this.route.params.subscribe((params: any) => { this.tvdbId = params.tvdbId; @@ -44,6 +50,11 @@ export class TvDetailsComponent implements OnInit { public async load() { this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + + if (this.isAdmin) { + this.showAdvanced = await this.sonarrService.isEnabled(); + } + if (this.fromSearch) { this.tv = await this.searchService.getTvInfoWithMovieDbId(this.tvdbId); this.tvdbId = this.tv.id; @@ -54,14 +65,15 @@ export class TvDetailsComponent implements OnInit { if (this.tv.requestId) { this.tvRequest = await this.requestService.getChildRequests(this.tv.requestId).toPromise(); this.showRequest = this.tvRequest.length > 0 ? this.tvRequest[0].parentRequest : undefined; + this.loadAdvancedInfo(); } 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 async request(userId: string) { + this.dialog.open(EpisodeRequestComponent, { width: "800px", data: { series: this.tv, requestOnBehalf: userId }, panelClass: 'modal-panel' }) } public async issue() { @@ -81,14 +93,61 @@ export class TvDetailsComponent implements OnInit { }); } + public async openAdvancedOptions() { + const dialog = this.dialog.open(TvAdvancedOptionsComponent, { width: "700px", data: { tvRequest: this.showRequest }, panelClass: 'modal-panel' }) + await dialog.afterClosed().subscribe(async result => { + if (result) { + // get the name and ids + result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0]; + result.profile = result.profiles.filter(f => f.id === +result.profileId)[0]; + await this.requestService2.updateTvAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.showRequest.id }).toPromise(); + this.setAdvancedOptions(result); + } + }); + } + + public async openRequestOnBehalf() { + const dialog = this.dialog.open(RequestBehalfComponent, { width: "700px", panelClass: 'modal-panel' }) + await dialog.afterClosed().subscribe(async result => { + if (result) { + await this.request(result.id); + } + }); + } + public setAdvancedOptions(data: IAdvancedData) { this.advancedOptions = data; console.log(this.advancedOptions); if (data.rootFolderId) { - this.showRequest.qualityOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path; + this.showRequest.qualityOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name; } if (data.profileId) { - this.showRequest.rootPathOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name; + this.showRequest.rootPathOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path; } } + + private loadAdvancedInfo() { + const profile = this.sonarrService.getQualityProfilesWithoutSettings(); + const folders = this.sonarrService.getRootFoldersWithoutSettings(); + + forkJoin([profile, folders]).subscribe(x => { + const sonarrProfiles = x[0]; + const sonarrRootFolders = x[1]; + + const profile = sonarrProfiles.filter((p) => { + return p.id === this.showRequest.qualityOverride; + }); + if (profile.length > 0) { + this.showRequest.qualityOverrideTitle = profile[0].name; + } + + const path = sonarrRootFolders.filter((folder) => { + return folder.id === this.showRequest.rootFolder; + }); + if (path.length > 0) { + this.showRequest.rootPathOverrideTitle = path[0].path; + } + + }); + } } diff --git a/src/Ombi/ClientApp/src/app/media-details/media-details.component.scss b/src/Ombi/ClientApp/src/app/media-details/media-details.component.scss index f3c655c6f..037d9bd23 100644 --- a/src/Ombi/ClientApp/src/app/media-details/media-details.component.scss +++ b/src/Ombi/ClientApp/src/app/media-details/media-details.component.scss @@ -5,24 +5,15 @@ h1 { font-size: 1.5rem; } - .mobile-poster { - display: block; - position: absolute; - width: 70px; - left: 12px; - bottom: 2px; + .mobile-top-text { + margin-bottom: 170px; } + #info-wrapper .sidebar-poster { margin-top: -126px !important; } } -@media (max-width: 767px) { - #summary-wrapper { - height: 350px !important; - } -} - #summary-wrapper .full-screenshot, .summary-wrapper .full-screenshot, #watching-wrapper .full-screenshot, @@ -46,7 +37,7 @@ background-size: cover; background-position: 50% 10%; transition: all .5s; - height: 450px; + height: 600px; color: #fff; position: relative; } @@ -88,22 +79,11 @@ #summary-wrapper .summary .container, .summary-wrapper .summary .container { position: absolute; - bottom: 0; + bottom: 300px; left: 0; right: 0; } -#summary-wrapper, -.summary-wrapper { - background-color: #000; - background-size: cover; - background-position: 50% 10%; - transition: all .5s; - height: 550px; - color: #fff; - position: relative; -} - .grey-text { color: #999; } @@ -163,11 +143,11 @@ } .btn-spacing { - margin-right: 10px !important; + margin-right: 10px; } .spacing-below { - margin-bottom: 15px !important; + margin-bottom: 15px; } .left-seperator { @@ -185,12 +165,14 @@ } .media-icons { - color: mat-color($ombi-app-primary) !important; - padding: 1%; + color: #FFF; + padding: 5px; } .media-row { - padding-top: 2%; + position:absolute; + bottom:0; + margin-bottom:20px; } .cast-profile-img { @@ -200,7 +182,6 @@ - .small-middle-container { margin: auto; width: 95%; @@ -222,4 +203,140 @@ .no-info { text-align: center; padding-top: 15%; -} \ No newline at end of file +} + +.content-end { + text-align: end; +} + +.rating-small { + width: 1.3em; +} +.stream-small { + width: 3em; + border-radius: 1em; + margin-right: 10px; + margin-top: 5px; +} + +.social-icons-container{ + position:absolute; + top:84px; + right:0px; + width:100%; + background-color:rgba(15,23,31,.6); +} + +.social-icons-container-inner{ + text-align:right; + display:flex; + justify-content: flex-end; + padding-right:2.5% +} + +.viewon-btn { + background-color: transparent; + text-decoration: none; +} + +.viewon-btn.plex { + color: #E5A00D; + box-shadow: inset 0px 0px 0px 1px #e5a00d; +} +.viewon-btn.emby { + color: #52b54a; + box-shadow: inset 0px 0px 0px 1px #52b54a; +} +.viewon-btn.jellyfin { + color: #00a4dc; + box-shadow: inset 0px 0px 0px 1px #00a4dc; +} + +::ng-deep .p-carousel-indicators { + display: none !important; + } + + +#info-wrapper{ + margin-top:-200px; +} +.full-screenshot.enabled.overlay{ + background-image: linear-gradient(to bottom, transparent, 50%, $ombi-background-primary); +} + +.row.justify-content-center.justify-content-sm-start.header-container{ + flex-wrap: nowrap; +} + +.details-button-container{ + width:100%; + position:relative; +} + +.info-wrapper .row{ + flex-wrap:wrap; +} + +.rating{ + display:flex; + justify-content: space-evenly; + width:100%; + flex-wrap: wrap; +} + +.left-panel-details .label{ + font-weight:500; +} + +.left-panel-details{ + font-weight:100; +} + +.genre-button-container .mat-chip-list .mat-chip-list-wrapper{ + margin-top:3px; + margin:0; + margin-left: -6px; +} + +.keyword-button-container .mat-chip-list .mat-chip-list-wrapper{ + margin-top:3px; + margin:0; + margin-left: -6px; +} + +.mat-card-header{ + font-size: 20px; + padding-top: 10px; + padding-bottom:10px; +} + +.media-row .mat-raised-button{ + padding:2px 1.5em;; + width:170px; + margin-top:10px; +} + +@media (max-width:500px){ + .row.justify-content-center.justify-content-sm-start.header-container{ + flex-wrap:wrap; + } + + .media-row{ + position:relative; + justify-content: center; + display: flex; + flex-wrap: wrap; + } + + .media-row .mat-raised-button{ + width:100%; + } + + .media-row .btn-spacing{ + margin-right:0; + } + + .media-row span{ + width:100%; + } +} 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 2544a4935..67c0436ad 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,8 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { SearchService, RequestService, RadarrService } from "../services"; - import {CarouselModule} from 'primeng/carousel'; import { SharedModule } from "../shared/shared.module"; @@ -13,6 +11,7 @@ import { PipeModule } from "../pipes/pipe.module"; import * as fromComponents from './components'; import { AuthGuard } from "../auth/auth.guard"; import { ArtistDetailsComponent } from "./components/artist/artist-details.component"; +import { ReactiveFormsModule } from "@angular/forms"; const routes: Routes = [ @@ -25,6 +24,7 @@ const routes: Routes = [ imports: [ RouterModule.forChild(routes), SharedModule, + ReactiveFormsModule, PipeModule, CarouselModule, ], diff --git a/src/Ombi/ClientApp/src/app/my-nav/SearchFilter.ts b/src/Ombi/ClientApp/src/app/my-nav/SearchFilter.ts index 40af3af79..c0a1f3a9b 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/SearchFilter.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/SearchFilter.ts @@ -4,4 +4,7 @@ export class SearchFilter { tvShows: boolean; music: boolean; people: boolean; + public constructor(init?:Partial) { + Object.assign(this, init); +} } 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 849fc9f21..86e037002 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 @@ -3,13 +3,13 @@ [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'" [mode]="(isHandset$ | async) ? 'over' : 'side'" [opened]="!(isHandset$ | async)"> - {{applicationName}} + {{applicationName}}
+ src="https://www.gravatar.com/avatar/{{emailHash}}?d={{applicationLogo ? applicationLogo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png'}}" />

{{username}}

@@ -29,7 +29,7 @@ {{nav.icon}} -  {{nav.name | translate}} @@ -42,11 +42,6 @@
- - wb_incandescent - brightness_4 -  {{ 'NavigationBar.ChangeTheme' | translate }} - exit_to_app @@ -65,22 +60,22 @@
- - + + -
+ -
+
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 index b8abd28dc..27346079b 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -5,10 +5,6 @@ overflow:auto; } -.sidenav { - width: 220px; -} - .sidenav .mat-toolbar { background: inherit; } @@ -44,11 +40,6 @@ padding: 0px 5px; } -.active-list-item { - background: $accent !important; - color:white; -} - ::ng-deep .dark .active-list-item { background: $accent-dark !important; color:black !important; @@ -100,16 +91,9 @@ } .profile-img img { - border-radius: 6px; - box-shadow: 4px 3px 6px 0 rgba(0,0,0,.2); width: 45px; } -.outer-profile { - background-color: #303030; - box-shadow: inset 0 0 3px #000000; - padding: 1px; -} .full { width: 100%; @@ -119,4 +103,79 @@ bottom: 0; left: 0; text-align: center; +} +// New CSS for new style // + +.sidenav-container .sidenav{ + background: $ombi-background-accent; + color:#FFF; + font-family: 'Montserrat', sans-serif; + + .application-name{ + font-family: 'Montserrat', sans-serif; + text-transform: uppercase; + color: $ombi-active; + align-items:center; + justify-content: center; + font-weight: 700; + font-size:36px; + padding:40px 20px; + height:auto; + max-width: 350px; + display: flex; + white-space: normal; + text-align: center; + } + + .outer-profile { + background: inherit; + box-shadow:none; + } + + .mat-list-item{ + color:#FFF; + font-family:Roboto, sans-serif; + font-size: 16px; + font-weight: 400; + padding:10px 20px; + height:auto; + width:20rem; + margin-bottom:0.5rem; + } + + .active-list-item{ + color:$ombi-background-accent; + background: $ombi-active; + border-radius:0px 30px 30px 0px; + padding:10px 20px; + height:auto; + width:20rem; + } +} + +.outer-profile .profile-img-container { + background: inherit; +} + +.content-container{ + background: $ombi-background-primary; + color:#FFF; + + .top-bar-container{ + background: $ombi-background-primary; + color:$ombi-background-primary-accent; + } + +} + +.top-search-bar{ + margin-left: 30px; +} + +::ng-deep .mat-toolbar-row, .mat-toolbar-single-row{ + height:auto; +} + +.top-search-bar{ + height:84px; } \ 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 553dcbf2c..d7216d114 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 @@ -9,6 +9,7 @@ import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { SearchFilter } from './SearchFilter'; import { Md5 } from 'ts-md5/dist/md5'; import { RequestType } from '../interfaces'; +import { FilterService } from '../discover/services/filter-service'; export enum SearchFilterType { Movie = 1, @@ -24,7 +25,7 @@ export enum SearchFilterType { }) export class MyNavComponent implements OnInit { - isHandset$: Observable = this.breakpointObserver.observe(Breakpoints.Handset) + isHandset$: Observable = this.breakpointObserver.observe([Breakpoints.Small, Breakpoints.Handset, Breakpoints.XSmall]) .pipe( map(result => result.matches) ); @@ -36,7 +37,6 @@ export class MyNavComponent implements OnInit { @Input() public isAdmin: string; @Input() public email: string; @Output() public logoutClick = new EventEmitter(); - @Output() public themeChange = new EventEmitter(); public theme: string; public issuesEnabled: boolean = false; public navItems: INavBar[]; @@ -48,7 +48,8 @@ export class MyNavComponent implements OnInit { constructor(private breakpointObserver: BreakpointObserver, private settingsService: SettingsService, - private store: StorageService) { + private store: StorageService, + private filterService: FilterService) { } public async ngOnInit() { @@ -76,6 +77,7 @@ export class MyNavComponent implements OnInit { var filter = this.store.get("searchFilter"); if (filter) { this.searchFilter = Object.assign(new SearchFilter(), JSON.parse(filter)); + this.filterService.changeFilter(this.searchFilter); } this.navItems = [ { name: "NavigationBar.Discover", icon: "find_replace", link: "/discover", requiresAdmin: false, enabled: true, faIcon: null }, @@ -85,7 +87,7 @@ export class MyNavComponent implements OnInit { // { name: "NavigationBar.Calendar", icon: "calendar_today", link: "/calendar", requiresAdmin: false, enabled: true }, { name: "NavigationBar.Donate", icon: "attach_money", link: "https://www.paypal.me/PlexRequestsNet", externalLink: true, requiresAdmin: true, enabled: true, toolTip: true, style: "color:red;", toolTipMessage: 'NavigationBar.DonateTooltip', faIcon: null }, { name: "NavigationBar.Donate", icon: "attach_money", link: customizationSettings.customDonationUrl, externalLink: true, requiresAdmin: false, enabled: customizationSettings.enableCustomDonations, toolTip: true, toolTipMessage: customizationSettings.customDonationMessage, faIcon: null }, - { name: "NavigationBar.FeatureSuggestion", icon: null, link: "https://features.ombi.io/", externalLink: true, requiresAdmin: true, enabled: true, toolTip: true, toolTipMessage: 'NavigationBar.FeatureSuggestionTooltip', faIcon: "fa-lightbulb-o" }, + { name: "NavigationBar.FeatureSuggestion", icon: null, link: "https://features.ombi.io/", externalLink: true, requiresAdmin: true, enabled: true, toolTip: true, toolTipMessage: 'NavigationBar.FeatureSuggestionTooltip', faIcon: "fa-lightbulb" }, { name: "NavigationBar.Settings", icon: "settings", link: "/Settings/About", requiresAdmin: true, enabled: true, faIcon: null }, { name: "NavigationBar.UserPreferences", icon: "person", link: "/user-preferences", requiresAdmin: false, enabled: true, faIcon: null }, ]; @@ -95,20 +97,6 @@ export class MyNavComponent implements OnInit { 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 changeFilter(event: MatSlideToggleChange, searchFilterType: SearchFilterType) { switch (searchFilterType) { case SearchFilterType.Movie: @@ -124,6 +112,7 @@ export class MyNavComponent implements OnInit { this.searchFilter.people = event.checked; break; } + this.filterService.changeFilter(this.searchFilter); this.store.save("searchFilter", JSON.stringify(this.searchFilter)); } 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 ccf574725..da8fa5d96 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,42 +1,5 @@ - -
- + - - - - - - - - - -   - {{result.title}} - - -   - - {{result.title}} - - - -   - - {{result.title}} - - - -   - {{result.title}} - - - -
\ 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 31245eb0e..3cd5021ed 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 @@ -31,17 +31,45 @@ $ombi-accent: #258a6d; background-color: white; } -::ng-deep .mat-form-field-underline{ - bottom:0.5em; + +::ng-deep .top-search-bar .mat-form-field-underline{ + display:none; +} + +::ng-deep .top-search-bar .mat-form-field-flex{ + border: 1px solid #35465c; + border-radius:30px; + margin-top:20px; + height:50px; } +::ng-deep .top-search-bar .mat-form-field-infix{ + margin-left:20px; + border-top:0px; + margin-top:5px; +} + +::ng-deep .top-search-bar .mat-form-field-appearance-legacy .mat-form-field-wrapper{ + padding-bottom:0px; +} .option-img { vertical-align: middle; margin-right: 8px; } - .options { - margin-bottom: 5px; - } +.options { + margin-bottom: 5px; +} + +::ng-deep .discoverResults{ + margin-top:40px; +} + +::ng-deep button:focus{ + outline:none; +} +::ng-deep .col-2.top-filter{ + margin-top:10px; +} \ 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 9467f9544..4d4573cd7 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 @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { debounceTime, switchMap, @@ -7,12 +7,8 @@ import { } from "rxjs/operators"; import { empty} from "rxjs"; -import { SearchV2Service } from "../services/searchV2.service"; -import { IMultiSearchResult } from "../interfaces"; import { Router } from "@angular/router"; import { FormGroup, FormBuilder } from "@angular/forms"; -import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; -import { SearchFilter } from "./SearchFilter"; @Component({ selector: "app-nav-search", @@ -20,15 +16,10 @@ import { SearchFilter } from "./SearchFilter"; styleUrls: ["./nav-search.component.scss"], }) export class NavSearchComponent implements OnInit { - @Input() public filter: SearchFilter; - public selectedItem: string; - public results: IMultiSearchResult[]; - public searching = false; public searchForm: FormGroup; constructor( - private searchService: SearchV2Service, private router: Router, private fb: FormBuilder ) {} @@ -41,38 +32,14 @@ export class NavSearchComponent implements OnInit { this.searchForm .get("input") .valueChanges.pipe( - debounceTime(600), - tap(() => (this.searching = true)), + debounceTime(1300), switchMap((value: string) => { if (value) { - return this.searchService - .multiSearch(value, this.filter) - .pipe(finalize(() => (this.searching = false))); + this.router.navigate([`discover`, value]); } - return empty().pipe(finalize(() => (this.searching = false))); + return empty();; }) ) - .subscribe((r) => (this.results = r)); - } - - public selected(event: MatAutocompleteSelectedEvent) { - this.searchForm.controls.input.setValue(null); - const val = event.option.value as IMultiSearchResult; - if (val.mediaType == "movie") { - this.router.navigate([`details/movie/${val.id}`]); - return; - } else if (val.mediaType == "tv") { - this.router.navigate([`details/tv/${val.id}/true`]); - return; - } else if (val.mediaType == "person") { - this.router.navigate([`discover/actor/${val.id}`]); - return; - } else if (val.mediaType == "Artist") { - this.router.navigate([`details/artist/${val.id}`]); - return; - } - } - displayFn(result: IMultiSearchResult) { - if (result) { return result.title; } + .subscribe(); } } diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/albums-grid/albums-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/albums-grid/albums-grid.component.html index 63e771e89..95b88a4f4 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/albums-grid/albums-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/albums-grid/albums-grid.component.html @@ -28,7 +28,7 @@
- +
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 index c32ceea1b..9c71884e0 100644 --- 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 @@ -28,8 +28,22 @@ -
+
+ + + + @@ -71,4 +85,11 @@
+ + + + + + {{ 'Requests.RequestsTitle' | translate}}
-
\ No newline at end of file +
+ + + + + + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.scss b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.scss index fd5a5e47c..c911bec1b 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.scss +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.scss @@ -4,7 +4,7 @@ background: $accent-dark !important; font-size: 1em; font-weight: bold; - color: #303030; + color: $ombi-background-primary-accent; } .mat-form-field { @@ -23,7 +23,7 @@ ::ng-deep .dark .mat-tab-label-active{ background: $accent-dark !important; - color: #303030 !important; + color: $ombi-background-primary-accent !important; font-weight:bold; } 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 index 93cf37a64..c73084c2b 100644 --- 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 @@ -1,14 +1,18 @@ import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef, OnInit } from "@angular/core"; -import { IMovieRequests, IRequestsViewModel } from "../../../interfaces"; +import { IMovieRequests, IRequestEngineResult, IRequestsViewModel } from "../../../interfaces"; import { MatPaginator } from "@angular/material/paginator"; import { MatSort } from "@angular/material/sort"; -import { merge, Observable, of as observableOf } from 'rxjs'; +import { merge, Observable, of as observableOf, forkJoin } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { RequestServiceV2 } from "../../../services/requestV2.service"; import { AuthService } from "../../../auth/auth.service"; import { StorageService } from "../../../shared/storage/storage-service"; import { RequestFilterType } from "../../models/RequestFilterType"; +import { SelectionModel } from "@angular/cdk/collections"; +import { NotificationService, RequestService } from "../../../services"; +import { TranslateService } from "@ngx-translate/core"; +import { MatTableDataSource } from "@angular/material/table"; @Component({ templateUrl: "./movies-grid.component.html", @@ -16,7 +20,7 @@ import { RequestFilterType } from "../../models/RequestFilterType"; styleUrls: ["./movies-grid.component.scss"] }) export class MoviesGridComponent implements OnInit, AfterViewInit { - public dataSource: IMovieRequests[] = []; + public dataSource: MatTableDataSource; public resultsLength: number; public isLoadingResults = true; public displayedColumns: string[] = ['title', 'requestedUser.requestedBy', 'status', 'requestStatus','requestedDate', 'actions']; @@ -25,6 +29,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { public defaultSort: string = "requestedDate"; public defaultOrder: string = "desc"; public currentFilter: RequestFilterType = RequestFilterType.All; + public selection = new SelectionModel(true, []); public RequestFilter = RequestFilterType; @@ -40,13 +45,17 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { @ViewChild(MatSort) sort: MatSort; constructor(private requestService: RequestServiceV2, private ref: ChangeDetectorRef, - private auth: AuthService, private storageService: StorageService) { + private auth: AuthService, private storageService: StorageService, + private requestServiceV1: RequestService, private notification: NotificationService, + private translateService: TranslateService) { } - - public ngOnInit() { - this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + public ngOnInit() { + this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + if (this.isAdmin) { + this.displayedColumns.unshift('select'); + } const defaultCount = this.storageService.get(this.storageKeyGridCount); const defaultSort = this.storageService.get(this.storageKey); const defaultOrder = this.storageService.get(this.storageKeyOrder); @@ -96,7 +105,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { this.isLoadingResults = false; return observableOf([]); }) - ).subscribe(data => this.dataSource = data); + ).subscribe(data => this.dataSource = new MatTableDataSource(data)); } public loadData(): Observable> { @@ -117,9 +126,9 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { public openOptions(request: IMovieRequests) { const filter = () => { - this.dataSource = this.dataSource.filter((req) => { - return req.id !== request.id; - }) + this.dataSource.data = this.dataSource.data.filter((req) => { + return req.id !== request.id; + }); }; const onChange = () => { @@ -133,4 +142,56 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { this.currentFilter = type; this.ngAfterViewInit(); } -} + + public isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.dataSource.data.length; + return numSelected === numRows; + } + + public masterToggle() { + this.isAllSelected() ? + this.selection.clear() : + this.dataSource.data.forEach(row => this.selection.select(row)); + } + + public async bulkDelete() { + if (this.selection.isEmpty()) { + return; + } + let tasks = new Array(); + this.selection.selected.forEach((selected) => { + tasks.push(this.requestServiceV1.removeMovieRequestAsync(selected.id)); + }); + + await Promise.all(tasks); + + this.notification.success(this.translateService.instant('Requests.RequestPanel.Deleted')) + this.selection.clear(); + this.ngAfterViewInit(); + } + + public bulkApprove() { + if (this.selection.isEmpty()) { + return; + } + let tasks = new Array>(); + this.selection.selected.forEach((selected) => { + tasks.push(this.requestServiceV1.approveMovie({ id: selected.id })); + }); + + this.isLoadingResults = true; + forkJoin(tasks).subscribe((result: IRequestEngineResult[]) => { + this.isLoadingResults = false; + const failed = result.filter(x => !x.result); + if(failed.length > 0) { + this.notification.error("Some requests failed to approve: " + failed[0].errorMessage); + this.selection.clear(); + return; + } + this.notification.success(this.translateService.instant('Requests.RequestPanel.Approved')); + this.selection.clear(); + this.ngAfterViewInit(); + }) + } +} \ No newline at end of file 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 index f8a34118d..78fd3f91f 100644 --- 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 @@ -21,10 +21,6 @@ float:right; } -::ng-deep .mat-form-field-label{ - font-size: 1.2em; -} - ::ng-deep .mat-form-field-infix { width: 10em; margin-top:1em; @@ -62,4 +58,15 @@ .justify-content-md-center { justify-content: center !important; } -} \ No newline at end of file +} + +::ng-deep table.requests button{ + margin:5px; +} + +::ng-deep .floating-fab { + position: fixed; + right: 3%; + bottom: 6%; + z-index: 10; +} 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 index a612d5180..7e0902483 100644 --- 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 @@ -28,7 +28,7 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/requests/movierequests.component.html b/src/Ombi/ClientApp/src/app/requests/movierequests.component.html index 3f8485fce..ad82ffbb9 100644 --- a/src/Ombi/ClientApp/src/app/requests/movierequests.component.html +++ b/src/Ombi/ClientApp/src/app/requests/movierequests.component.html @@ -4,13 +4,13 @@ (keyup)="search($event)"> - - + +
{{RequestType[v.type] | humanize}} {{v.retryCount}}
diff --git a/src/Ombi/ClientApp/src/app/settings/issues/issues.component.html b/src/Ombi/ClientApp/src/app/settings/issues/issues.component.html index 702ca6915..e42dd3182 100644 --- a/src/Ombi/ClientApp/src/app/settings/issues/issues.component.html +++ b/src/Ombi/ClientApp/src/app/settings/issues/issues.component.html @@ -1,6 +1,6 @@ 
- +
Issues diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html new file mode 100644 index 000000000..ada4f5aa6 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html @@ -0,0 +1,117 @@ + + +
+
+
+ + Jellyfin Configuration + + +
+
+
+
+ Enable +
+
+
+
+ + +
+ +
+
+
+
+
+ + Server Name + + +
+ +
+ + Hostname / IP + + + +
+ + Server ID + + +
+ + Port + + + + + SSL + +
+
+ + API Key + + +
+
+ + Base URL + + +
+
+ + Externally Facing Hostname + + + + Current URL: "{{server.serverHostname}}/#!/itemdetails.html?id=1" + Current URL: "{{server.ssl ? "https://" : "http://"}}{{server.ip}}:{{server.port}}/" + +
+ + +
+
+ +
+
+
+
+ +
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.scss b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.scss new file mode 100644 index 000000000..649201f48 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.scss @@ -0,0 +1,41 @@ +@import "~styles/shared.scss"; +.small-middle-container { + margin: auto; + width: 95%; + margin-top: 10px; +} + +.col-md-10 { + display: grid; +} + +.col-md-2 { + display: contents; +} + +.control-label { + font-weight: 400; +} + +.row { + display: block; +} + +.btn-danger-outline { + background-color: #E84C3D; +} + +.btn-success-outline { + background-color: #1b9d1b; +} + +::ng-deep .dark .btn:hover { + box-shadow: 0 5px 11px 0 rgba(255, 255, 255, 0.18), 0 4px 15px 0 rgba(255, 255, 255, 0.15); + color: inherit; +} + +@media (min-width:1440px) { + .col-md-2 { + display: inline-table; + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts new file mode 100644 index 000000000..fd65ba43c --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts @@ -0,0 +1,104 @@ +import { Component, OnInit } from "@angular/core"; + +import { IJellyfinServer, IJellyfinSettings } from "../../interfaces"; +import { JellyfinService, JobService, NotificationService, SettingsService, TesterService } from "../../services"; +import { MatTabChangeEvent } from "@angular/material/tabs"; +import {FormControl} from '@angular/forms'; + +@Component({ + templateUrl: "./jellyfin.component.html", + styleUrls: ["./jellyfin.component.scss"] +}) +export class JellyfinComponent implements OnInit { + + public settings: IJellyfinSettings; + public hasDiscoveredOrDirty: boolean; + selected = new FormControl(0); + + + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private testerService: TesterService, + private jobService: JobService, + private jellyfinService: JellyfinService) { } + + public ngOnInit() { + this.settingsService.getJellyfin().subscribe(x => this.settings = x); + } + + public async discoverServerInfo(server: IJellyfinServer) { + const result = await this.jellyfinService.getPublicInfo(server).toPromise(); + server.name = result.serverName; + server.serverId = result.id; + this.hasDiscoveredOrDirty = true; + } + + public addTab(event: MatTabChangeEvent) { + const tabName = event.tab.textLabel; + if (tabName == "Add Server"){ + if (this.settings.servers == null) { + this.settings.servers = []; + } + this.settings.servers.push({ + name: "New " + this.settings.servers.length + "*", + id: Math.floor(Math.random() * (99999 - 0 + 1) + 1), + apiKey: "", + administratorId: "", + enableEpisodeSearching: false, + ip: "", + port: 0, + ssl: false, + subDir: "", + } as IJellyfinServer); + this.selected.setValue(this.settings.servers.length - 1); + } + } + + public toggle() { + this.hasDiscoveredOrDirty = true; + } + + public test(server: IJellyfinServer) { + this.testerService.jellyfinTest(server).subscribe(x => { + if (x === true) { + this.notificationService.success(`Successfully connected to the Jellyfin server ${server.name}!`); + } else { + this.notificationService.error(`We could not connect to the Jellyfin server ${server.name}!`); + } + }); + } + + public removeServer(server: IJellyfinServer) { + const index = this.settings.servers.indexOf(server, 0); + if (index > -1) { + this.settings.servers.splice(index, 1); + this.selected.setValue(this.settings.servers.length - 1); + } + } + + public save() { + this.settingsService.saveJellyfin(this.settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved Jellyfin settings"); + } else { + this.notificationService.success("There was an error when saving the Jellyfin settings"); + } + }); + } + + public runCacher(): void { + this.jobService.runJellyfinCacher().subscribe(x => { + if(x) { + this.notificationService.success("Triggered the Jellyfin Content Cacher"); + } + }); + } + + public clearDataAndResync(): void { + this.jobService.clearMediaserverData().subscribe(x => { + if (x) { + this.notificationService.success("Triggered the Clear MediaServer Resync"); + } + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.html b/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.html index 88c4bdfbb..03780b264 100644 --- a/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.html +++ b/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.html @@ -1,11 +1,11 @@  - +
Job Settings
Changes require a restart.

- You can generate valid CRON Expressions here: https://www.cronmaker.com/ + You can generate valid CRON Expressions here: https://www.cronmaker.com/

@@ -86,6 +86,14 @@
+
+ + Jellyfin Sync + + The Jellyfin Sync is required + +
+
User Importer diff --git a/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.ts b/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.ts index 914d126d0..83508e752 100644 --- a/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.ts @@ -24,6 +24,7 @@ export class JobsComponent implements OnInit { automaticUpdater: [x.automaticUpdater, Validators.required], couchPotatoSync: [x.couchPotatoSync, Validators.required], embyContentSync: [x.embyContentSync, Validators.required], + jellyfinContentSync: [x.jellyfinContentSync, Validators.required], plexContentSync: [x.plexContentSync, Validators.required], userImporter: [x.userImporter, Validators.required], sonarrSync: [x.sonarrSync, Validators.required], diff --git a/src/Ombi/ClientApp/src/app/settings/landingpage/landingpage.component.html b/src/Ombi/ClientApp/src/app/settings/landingpage/landingpage.component.html index 91b923833..fbec2a300 100644 --- a/src/Ombi/ClientApp/src/app/settings/landingpage/landingpage.component.html +++ b/src/Ombi/ClientApp/src/app/settings/landingpage/landingpage.component.html @@ -1,6 +1,6 @@ 
- +
Landing Page Configuration
diff --git a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html index 850902dd1..6117414bd 100644 --- a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html +++ b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html @@ -56,7 +56,7 @@
+ *ngIf="profilesRunning" class="fas fa-spinner fa-spin">
@@ -72,7 +72,7 @@
+ *ngIf="rootFoldersRunning" class="fas fa-spinner fa-spin">
@@ -90,7 +90,7 @@
+ *ngIf="metadataRunning" class="fas fa-spinner fa-spin">
diff --git a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts index 7ff770c4c..5d8667db4 100644 --- a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { ILidarrSettings, IMinimumAvailability, IProfiles, IRadarrProfile, IRadarrRootFolder } from "../../interfaces"; @@ -106,9 +106,11 @@ export class LidarrComponent implements OnInit { return; } const settings = form.value; - this.testerService.lidarrTest(settings).subscribe(x => { - if (x === true) { + this.testerService.lidarrTest(settings).subscribe(result => { + if (result.isValid) { this.notificationService.success("Successfully connected to Lidarr!"); + } else if (result.expectedSubDir !== null) { + this.notificationService.error("Your Lidarr Base URL must be set to " + result.expectedSubDir); } else { this.notificationService.error("We could not connect to Lidarr!"); } diff --git a/src/Ombi/ClientApp/src/app/settings/logs/logs.component.scss b/src/Ombi/ClientApp/src/app/settings/logs/logs.component.scss index 18096ace2..da5fdfd6e 100644 --- a/src/Ombi/ClientApp/src/app/settings/logs/logs.component.scss +++ b/src/Ombi/ClientApp/src/app/settings/logs/logs.component.scss @@ -9,5 +9,10 @@ } ::ng-deep .dark .code-block { - color:#FFF !important; + color:#FFF; +} + +::ng-deep .code-block { + color:#FFF; + font-family: Menlo, Monaco, "Courier New", Courier, monospace; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/massemail/massemail.component.html b/src/Ombi/ClientApp/src/app/settings/massemail/massemail.component.html index a858b98d8..225e4b860 100644 --- a/src/Ombi/ClientApp/src/app/settings/massemail/massemail.component.html +++ b/src/Ombi/ClientApp/src/app/settings/massemail/massemail.component.html @@ -1,6 +1,6 @@ - +
- +
Mass Email @@ -23,7 +23,7 @@
- +
@@ -48,4 +48,4 @@
-
\ No newline at end of file +
diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/newsletter.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/newsletter.component.html index 80aeba34f..4da0689eb 100644 --- a/src/Ombi/ClientApp/src/app/settings/notifications/newsletter.component.html +++ b/src/Ombi/ClientApp/src/app/settings/notifications/newsletter.component.html @@ -1,6 +1,6 @@  - +
Newsletter diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html index 02551a345..50d4b5c32 100644 --- a/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html +++ b/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html @@ -1,4 +1,4 @@ - +

diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/webhook.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/webhook.component.html index 1ee583104..8ab28cc3c 100644 --- a/src/Ombi/ClientApp/src/app/settings/notifications/webhook.component.html +++ b/src/Ombi/ClientApp/src/app/settings/notifications/webhook.component.html @@ -22,7 +22,7 @@
diff --git a/src/Ombi/ClientApp/src/app/settings/ombi/ombi.component.html b/src/Ombi/ClientApp/src/app/settings/ombi/ombi.component.html index 63d9ce2a1..d504869b5 100644 --- a/src/Ombi/ClientApp/src/app/settings/ombi/ombi.component.html +++ b/src/Ombi/ClientApp/src/app/settings/ombi/ombi.component.html @@ -1,6 +1,6 @@ 
- +
Ombi Configuration @@ -16,7 +16,7 @@ Api Key diff --git a/src/Ombi/ClientApp/src/app/settings/plex/plex.component.html b/src/Ombi/ClientApp/src/app/settings/plex/plex.component.html index eb143bccb..3b05e4585 100644 --- a/src/Ombi/ClientApp/src/app/settings/plex/plex.component.html +++ b/src/Ombi/ClientApp/src/app/settings/plex/plex.component.html @@ -74,7 +74,7 @@ Episode Batch Size
@@ -102,7 +102,7 @@
@@ -143,7 +143,7 @@
@@ -158,15 +158,21 @@
-
-
+
+ +
@@ -185,4 +191,4 @@
- \ No newline at end of file + diff --git a/src/Ombi/ClientApp/src/app/settings/plex/plex.component.ts b/src/Ombi/ClientApp/src/app/settings/plex/plex.component.ts index 7509fb45a..dc2352999 100644 --- a/src/Ombi/ClientApp/src/app/settings/plex/plex.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/plex/plex.component.ts @@ -73,10 +73,9 @@ export class PlexComponent implements OnInit, OnDestroy { } public addTab(event: MatTabChangeEvent) { - const tabName = event.tab.textLabel; if (tabName == "Add Server"){ - + if (this.settings.servers == null) { this.settings.servers = []; } @@ -146,6 +145,14 @@ export class PlexComponent implements OnInit, OnDestroy { }); } + public clearDataAndResync(): void { + this.jobService.clearMediaserverData().subscribe(x => { + if (x) { + this.notificationService.success("Triggered the Clear MediaServer Resync"); + } + }); + } + public ngOnDestroy() { this.subscriptions.next(); this.subscriptions.complete(); diff --git a/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.html b/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.html index a6016b73f..45963b1e8 100644 --- a/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.html +++ b/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.html @@ -59,7 +59,7 @@
- +
@@ -74,7 +74,7 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.ts b/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.ts index 905a72f36..df804a08c 100644 --- a/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/radarr/radarr.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { IMinimumAvailability, IRadarrProfile, IRadarrRootFolder } from "../../interfaces"; @@ -96,9 +96,11 @@ export class RadarrComponent implements OnInit { return; } const settings = form.value; - this.testerService.radarrTest(settings).subscribe(x => { - if (x === true) { + this.testerService.radarrTest(settings).subscribe(result => { + if (result.isValid) { this.notificationService.success("Successfully connected to Radarr!"); + } else if (result.expectedSubDir !== null) { + this.notificationService.error("Your Radarr Base URL must be set to " + result.expectedSubDir); } else { this.notificationService.error("We could not connect to Radarr!"); } diff --git a/src/Ombi/ClientApp/src/app/settings/settings.module.ts b/src/Ombi/ClientApp/src/app/settings/settings.module.ts index 58ceb9c64..b7f4536b5 100644 --- a/src/Ombi/ClientApp/src/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/src/app/settings/settings.module.ts @@ -8,7 +8,7 @@ import { ClipboardModule } from "ngx-clipboard"; import { AuthGuard } from "../auth/auth.guard"; import { AuthService } from "../auth/auth.service"; import { - CouchPotatoService, EmbyService, IssuesService, JobService, LidarrService, MobileService, NotificationMessageService, PlexService, RadarrService, + CouchPotatoService, EmbyService, JellyfinService, IssuesService, JobService, LidarrService, MobileService, NotificationMessageService, PlexService, RadarrService, RequestRetryService, SonarrService, TesterService, ValidationService, SystemService, FileDownloadService, TheMovieDbService } from "../services"; @@ -19,6 +19,7 @@ import { CouchPotatoComponent } from "./couchpotato/couchpotato.component"; import { CustomizationComponent } from "./customization/customization.component"; import { DogNzbComponent } from "./dognzb/dognzb.component"; import { EmbyComponent } from "./emby/emby.component"; +import { JellyfinComponent } from "./jellyfin/jellyfin.component"; import { FailedRequestsComponent } from "./failedrequests/failedrequests.component"; import { IssuesComponent } from "./issues/issues.component"; import { JobsComponent } from "./jobs/jobs.component"; @@ -73,6 +74,7 @@ const routes: Routes = [ { path: "About", component: AboutComponent, canActivate: [AuthGuard] }, { path: "Plex", component: PlexComponent, canActivate: [AuthGuard] }, { path: "Emby", component: EmbyComponent, canActivate: [AuthGuard] }, + { path: "Jellyfin", component: JellyfinComponent, canActivate: [AuthGuard] }, { path: "Sonarr", component: SonarrComponent, canActivate: [AuthGuard] }, { path: "Radarr", component: RadarrComponent, canActivate: [AuthGuard] }, { path: "LandingPage", component: LandingPageComponent, canActivate: [AuthGuard] }, @@ -131,6 +133,7 @@ const routes: Routes = [ OmbiComponent, PlexComponent, EmbyComponent, + JellyfinComponent, JobsComponent, LandingPageComponent, CustomizationComponent, @@ -182,6 +185,7 @@ const routes: Routes = [ IssuesService, PlexService, EmbyService, + JellyfinService, MobileService, NotificationMessageService, LidarrService, diff --git a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html index 20b843139..21e3feb46 100644 --- a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html @@ -1,5 +1,5 @@  - + @@ -7,36 +7,37 @@ - + - + - + + - + - + - + - + @@ -54,11 +55,11 @@ - + - + - \ No newline at end of file + diff --git a/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.html b/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.html index 2e2e13170..7ca71b554 100644 --- a/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.html +++ b/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.html @@ -61,7 +61,7 @@
+ Load Qualities
@@ -93,7 +93,7 @@
+ Load Folders
@@ -122,12 +122,12 @@
+ Languages
diff --git a/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.ts b/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.ts index f3cc71018..edd22bbdd 100644 --- a/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/sonarr/sonarr.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms"; import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder } from "../../interfaces"; @@ -150,9 +150,11 @@ export class SonarrComponent implements OnInit { return; } const settings = form.value; - this.testerService.sonarrTest(settings).subscribe(x => { - if (x) { + this.testerService.sonarrTest(settings).subscribe(result => { + if (result.isValid) { this.notificationService.success("Successfully connected to Sonarr!"); + } else if (result.expectedSubDir !== null) { + this.notificationService.error("Your Sonarr Base URL must be set to " + result.expectedSubDir); } else { this.notificationService.error("We could not connect to Sonarr!"); } diff --git a/src/Ombi/ClientApp/src/app/settings/update/update.component.html b/src/Ombi/ClientApp/src/app/settings/update/update.component.html index 6d0688697..d99cccf05 100644 --- a/src/Ombi/ClientApp/src/app/settings/update/update.component.html +++ b/src/Ombi/ClientApp/src/app/settings/update/update.component.html @@ -1,6 +1,6 @@ - + - +
Update Settings diff --git a/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.html index 870bd45ae..20bd9a970 100644 --- a/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.html @@ -1,6 +1,6 @@ 
- +
User Importer Settings
@@ -13,13 +13,13 @@ Import Plex Users
- +
Import Plex Admin

Plex Users excluded from Import

- +
@@ -27,14 +27,26 @@
Import Emby Users
- +

Emby Users excluded from Import

- +
+
+
+ Import Jellyfin Users +
+ +
+

Jellyfin Users excluded from Import

+ + +
+ +

Default Roles

@@ -63,6 +75,15 @@
+ + + + + {{value}} + + + +
-
\ No newline at end of file +
diff --git a/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.ts b/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.ts index 00483e4f2..7b2a5e3e0 100644 --- a/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.ts @@ -2,7 +2,7 @@ import { ICheckbox, IUserManagementSettings } from "../../interfaces"; import { IUsersModel } from "../../interfaces"; -import { EmbyService, IdentityService, JobService, NotificationService, PlexService, SettingsService } from "../../services"; +import { EmbyService, JellyfinService, IdentityService, JobService, NotificationService, PlexService, SettingsService } from "../../services"; @Component({ templateUrl: "./usermanagement.component.html", @@ -12,6 +12,7 @@ export class UserManagementComponent implements OnInit { public plexEnabled: boolean; public embyEnabled: boolean; + public jellyfinEnabled: boolean; public settings: IUserManagementSettings; public claims: ICheckbox[]; @@ -23,21 +24,28 @@ export class UserManagementComponent implements OnInit { public filteredEmbyUsers: IUsersModel[]; public bannedEmbyUsers: IUsersModel[] = []; + public jellyfinUsers: IUsersModel[]; + public filteredJellyfinUsers: IUsersModel[]; + public bannedJellyfinUsers: IUsersModel[] = []; + public enableImportButton = false; + public countries: string[]; constructor(private readonly settingsService: SettingsService, private readonly notificationService: NotificationService, private readonly identityService: IdentityService, private readonly plexService: PlexService, private readonly jobService: JobService, - private readonly embyService: EmbyService) { + private readonly embyService: EmbyService, + private readonly jellyfinService: JellyfinService) { } public ngOnInit(): void { + this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x); this.settingsService.getUserManagementSettings().subscribe(x => { this.settings = x; - if(x.importEmbyUsers || x.importPlexUsers) { + if(x.importEmbyUsers || x.importJellyfinUsers || x.importPlexUsers) { this.enableImportButton = true; } @@ -65,6 +73,18 @@ export class UserManagementComponent implements OnInit { }); }); + this.jellyfinService.getUsers().subscribe(f => { + this.jellyfinUsers = f; + this.jellyfinUsers.forEach((jellyfin) => { + const isExcluded = this.settings.bannedPlexUserIds.some((val) => { + return jellyfin.id === val; + }); + if (isExcluded) { + this.bannedJellyfinUsers.push(jellyfin); + } + }); + }); + this.identityService.getAllAvailableClaims().subscribe(c => { this.claims = c; @@ -80,6 +100,7 @@ export class UserManagementComponent implements OnInit { }); this.settingsService.getPlex().subscribe(x => this.plexEnabled = x.enable); this.settingsService.getEmby().subscribe(x => this.embyEnabled = x.enable); + this.settingsService.getJellyfin().subscribe(x => this.jellyfinEnabled = x.enable); } public submit(): void { @@ -89,8 +110,9 @@ export class UserManagementComponent implements OnInit { this.settings.defaultRoles = enabledClaims.map((claim) => claim.value); this.settings.bannedPlexUserIds = this.bannedPlexUsers.map((u) => u.id); this.settings.bannedEmbyUserIds = this.bannedEmbyUsers.map((u) => u.id); + this.settings.bannedJellyfinUserIds = this.bannedJellyfinUsers.map((u) => u.id); - if(this.settings.importEmbyUsers || this.settings.importPlexUsers) { + if(this.settings.importEmbyUsers || this.settings.importJellyfinUsers || this.settings.importPlexUsers) { this.enableImportButton = true; } @@ -111,10 +133,15 @@ export class UserManagementComponent implements OnInit { this.filteredEmbyUsers = this.filter(event.query, this.embyUsers); } + public filterJellyfinList(event: any) { + this.filteredJellyfinUsers = this.filter(event.query, this.jellyfinUsers); + } + public runImporter(): void { this.jobService.runPlexImporter().subscribe(); this.jobService.runEmbyImporter().subscribe(); + this.jobService.runJellyfinImporter().subscribe(); } private filter(query: string, users: IUsersModel[]): IUsersModel[] { diff --git a/src/Ombi/ClientApp/src/app/settings/vote/vote.component.html b/src/Ombi/ClientApp/src/app/settings/vote/vote.component.html index a556fa048..c126bec0d 100644 --- a/src/Ombi/ClientApp/src/app/settings/vote/vote.component.html +++ b/src/Ombi/ClientApp/src/app/settings/vote/vote.component.html @@ -1,6 +1,6 @@ 
- +
Vote diff --git a/src/Ombi/ClientApp/src/app/settings/wiki.component.html b/src/Ombi/ClientApp/src/app/settings/wiki.component.html index 52cedf6ea..d34ce82ff 100644 --- a/src/Ombi/ClientApp/src/app/settings/wiki.component.html +++ b/src/Ombi/ClientApp/src/app/settings/wiki.component.html @@ -1,14 +1,14 @@ 
- + diff --git a/src/Ombi/ClientApp/src/app/settings/wiki.component.ts b/src/Ombi/ClientApp/src/app/settings/wiki.component.ts index d2187bb8a..f391713cd 100644 --- a/src/Ombi/ClientApp/src/app/settings/wiki.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/wiki.component.ts @@ -5,6 +5,7 @@ templateUrl: "./wiki.component.html", }) export class WikiComponent { - @Input() public url: string; + @Input() public path: string; @Input() public text: string; + public domain: string = "http://docs.ombi.app/" } 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 index f57f014c9..327d56723 100644 --- 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 @@ -1,9 +1,8 @@ -
- +
+
+ {{'MediaDetails.EpisodeSelector.NoEpisodes' | translate}}
-
- -
+
@@ -16,16 +15,16 @@
-
+
- + Season {{season.seasonNumber}} - Season {{season.seasonNumber}} + Season {{season.seasonNumber}} - Description + @@ -49,14 +48,7 @@ {{ep.airDateDisplay }}
- {{'Common.Available' | translate}} - {{'Common.ProcessingRequest' | translate}} - Selected - {{'Common.PendingApproval' | translate}} - - Not Requested + {{ep.requestStatus | translate}}
@@ -71,7 +63,7 @@
-
+
+
+ +
+
+ {{'UserPreferences.StreamingCountryDescription' | translate}} +
+ + + + + {{value}} + + + +
+
+ +
+ + +
\ 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 index 3fe4359eb..7969bdd45 100644 --- 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 @@ -3,8 +3,8 @@ 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"; -import { IdentityService, SettingsService } from "../../../services"; -import { IUser } from "../../../interfaces"; +import { IdentityService, NotificationService, SettingsService } from "../../../services"; +import { ICustomizationSettings, IUser } from "../../../interfaces"; @Component({ templateUrl: "./user-preference.component.html", @@ -17,12 +17,15 @@ export class UserPreferenceComponent implements OnInit { public availableLanguages = AvailableLanguages; public qrCode: string; public qrCodeEnabled: boolean; + public countries: string[]; + public selectedCountry: string; + public customizationSettings: ICustomizationSettings; private user: IUser; constructor(private authService: AuthService, private readonly translate: TranslateService, - private storage: StorageService, + private readonly notification: NotificationService, private readonly identityService: IdentityService, private readonly settingsService: SettingsService) { } @@ -31,26 +34,41 @@ export class UserPreferenceComponent implements OnInit { if (user.name) { this.username = user.name; } - const customization = await this.settingsService.getCustomization().toPromise(); + this.customizationSettings = await this.settingsService.getCustomization().toPromise(); + + this.selectedLang = this.translate.currentLang; const accessToken = await this.identityService.getAccessToken().toPromise(); - this.qrCode = `${customization.applicationUrl}|${accessToken}`; + this.qrCode = `${this.customizationSettings.applicationUrl}|${accessToken}`; - if(!customization.applicationUrl) { + if(!this.customizationSettings.applicationUrl) { this.qrCodeEnabled = false; } else { this.qrCodeEnabled = true; } this.user = await this.identityService.getUser().toPromise(); - if (this.user.language) { - this.selectedLang = this.user.language; - } + this.selectedCountry = this.user.streamingCountry; + this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x); + this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); + } public languageSelected() { - this.identityService.updateLanguage(this.selectedLang).subscribe(); + this.identityService.updateLanguage(this.selectedLang).subscribe(x => this.notification.success(this.translate.instant("UserPreferences.Updated"))); this.translate.use(this.selectedLang); } + public countrySelected() { + this.identityService.updateStreamingCountry(this.selectedCountry).subscribe(x => this.notification.success(this.translate.instant("UserPreferences.Updated"))); + } + + public openMobileApp(event: any) { + event.preventDefault(); + + this.identityService.getAccessToken().subscribe(x => { + const url = `ombi://${this.customizationSettings.applicationUrl}_${x}`; + window.location.assign(url); + }); + } } 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 d03bb9d2e..64f5d76d2 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html @@ -18,6 +18,14 @@
+ + + + + {{value}} + + +
diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts index db0403a01..486f456d0 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { AfterViewInit, Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ICheckbox, INotificationAgent, INotificationPreferences, IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUser, UserType } from "../interfaces"; @@ -25,6 +25,8 @@ export class UserManagementUserComponent implements OnInit { public NotificationAgent = INotificationAgent; public edit: boolean; + public countries: string[]; + constructor(private identityService: IdentityService, private notificationService: MessageService, private router: Router, @@ -45,6 +47,8 @@ export class UserManagementUserComponent implements OnInit { } public ngOnInit() { + + this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x); this.identityService.getAllAvailableClaims().subscribe(x => this.availableClaims = x); if(this.edit) { this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => this.notificationPreferences = x); @@ -74,6 +78,7 @@ export class UserManagementUserComponent implements OnInit { episodeRequestQuota: null, movieRequestQuota: null, language: null, + streamingCountry: "US", userQualityProfiles: { radarrQualityProfile: 0, radarrRootPath: 0, @@ -172,7 +177,7 @@ export class UserManagementUserComponent implements OnInit { } }); } - + public back() { this.location.back(); } diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html index af789e168..b17b35dfd 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html @@ -87,7 +87,10 @@ Local User Plex User - Emby User + Emby User + Emby Connect User + Jellyfin User + @@ -122,17 +125,15 @@ -

Bulk Edit

-
+
-
- {{c.value | humanize}} + {{c.value | humanize}}
- +
@@ -147,6 +148,14 @@ Music Request Limit + + + + + {{value}} + + + diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.scss b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.scss index 19dab627d..4f5477693 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.scss +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.scss @@ -22,10 +22,6 @@ float:right; } -::ng-deep .mat-form-field-label{ - font-size: 1.2em; -} - ::ng-deep .mat-form-field-infix { width: 10em; margin-top:1em; diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.ts b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.ts index b412091a8..66268082b 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.ts +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.ts @@ -27,8 +27,11 @@ export class UserManagementComponent implements OnInit { public bulkMovieLimit?: number; public bulkEpisodeLimit?: number; public bulkMusicLimit?: number; + public bulkStreaming?: string; public plexEnabled: boolean; + public countries: string[]; + constructor(private identityService: IdentityService, private settingsService: SettingsService, private notificationService: NotificationService, @@ -38,6 +41,7 @@ export class UserManagementComponent implements OnInit { public async ngOnInit() { + this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x); this.users = await this.identityService.getUsers().toPromise(); this.dataSource = new MatTableDataSource(this.users); this.dataSource.sort = this.sort; diff --git a/src/Ombi/ClientApp/src/app/vote/vote.component.html b/src/Ombi/ClientApp/src/app/vote/vote.component.html index d91ce85bd..473953549 100644 --- a/src/Ombi/ClientApp/src/app/vote/vote.component.html +++ b/src/Ombi/ClientApp/src/app/vote/vote.component.html @@ -6,12 +6,12 @@
  • {{ 'Votes.CompletedVotesTab' | translate }} + class="far fa-smile"> {{ 'Votes.CompletedVotesTab' | translate }}
  • @@ -32,9 +32,9 @@ - - - -
    - - - +
    +
    + +
    +
    +
    +

    Protect your Ombi

    + Create an Admin account to make sure you are always able to access your Ombi. +
    + + + +
    + + + +
    + You'll need to configure e-mail to reset your password! +
    +
    diff --git a/src/Ombi/ClientApp/src/app/wizard/createadmin/createadmin.component.ts b/src/Ombi/ClientApp/src/app/wizard/createadmin/createadmin.component.ts index e691ca627..ec87a5668 100644 --- a/src/Ombi/ClientApp/src/app/wizard/createadmin/createadmin.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/createadmin/createadmin.component.ts @@ -4,6 +4,7 @@ import { ICreateWizardUser } from "../../interfaces"; @Component({ selector: "wizard-local-admin", templateUrl: "./createadmin.component.html", + styleUrls: ["../welcome/welcome.component.scss"] }) export class CreateAdminComponent { diff --git a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html index c5d99d5a4..53f4bd161 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html @@ -1,31 +1,40 @@ - -
    -
    -
    -
    - - - -
    +
    +
    + +
    +
    +
    +

    Emby Configuration

    +
    +
    +
    +
    + + + +
    -
    +
    - - - -
    -
    - Enable SSL -
    -
    - - - - -
    -
    - Save
    + + + +
    +
    + Enable SSL +
    +
    + + + + +
    +
    +
    +
    +
    -
    \ No newline at end of file +
    +
    diff --git a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts index 58d5dd3f6..fb0c167a6 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts @@ -8,6 +8,7 @@ import { IEmbySettings } from "../../interfaces"; @Component({ selector: "wizard-emby", templateUrl: "./emby.component.html", + styleUrls: ["../welcome/welcome.component.scss"], }) export class EmbyComponent implements OnInit { @@ -20,7 +21,6 @@ export class EmbyComponent implements OnInit { public ngOnInit() { this.embySettings = { servers: [], - isJellyfin: false, id: 0, enable: true, }; diff --git a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html new file mode 100644 index 000000000..653e54876 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html @@ -0,0 +1,40 @@ +
    +
    + +
    +
    +
    +

    Jellyfin Configuration

    +
    +
    +
    +
    + + + +
    + +
    + + + + +
    +
    + Enable SSL +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts new file mode 100644 index 000000000..f3779b6be --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts @@ -0,0 +1,52 @@ +import { Component, OnInit } from "@angular/core"; + +import { JellyfinService } from "../../services"; +import { NotificationService } from "../../services"; + +import { IJellyfinSettings } from "../../interfaces"; + +@Component({ + selector: "wizard-jellyfin", + templateUrl: "./jellyfin.component.html", + styleUrls: ["../welcome/welcome.component.scss"] +}) +export class JellyfinComponent implements OnInit { + + public jellyfinSettings: IJellyfinSettings; + + constructor(private jellyfinService: JellyfinService, + private notificationService: NotificationService) { + } + + public ngOnInit() { + this.jellyfinSettings = { + servers: [], + id: 0, + enable: true, + }; + this.jellyfinSettings.servers.push({ + ip: "", + administratorId: "", + id: 0, + apiKey: "", + enableEpisodeSearching: false, + name: "Default", + port: 8096, + ssl: false, + subDir: "", + serverHostname: "", + serverId: undefined + }); + } + + public save() { + this.jellyfinService.logIn(this.jellyfinSettings).subscribe(x => { + if (x == null || !x.servers[0].apiKey) { + this.notificationService.error("Username or password was incorrect. Could not authenticate with Jellyfin."); + return; + } + + this.notificationService.success("Done! Please press next"); + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/wizard/mediaserver/mediaserver.component.html b/src/Ombi/ClientApp/src/app/wizard/mediaserver/mediaserver.component.html index fd561a227..afa626f0d 100644 --- a/src/Ombi/ClientApp/src/app/wizard/mediaserver/mediaserver.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/mediaserver/mediaserver.component.html @@ -11,6 +11,13 @@
    +
    +
    + + + +
    +
    -
    \ No newline at end of file +
    diff --git a/src/Ombi/ClientApp/src/app/wizard/models/OmbiConfigModel.ts b/src/Ombi/ClientApp/src/app/wizard/models/OmbiConfigModel.ts new file mode 100644 index 000000000..29e6e68a9 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/models/OmbiConfigModel.ts @@ -0,0 +1,5 @@ +export interface IOmbiConfigModel { + applicationName: string; + applicationUrl: string; + logo: string; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/wizard/ombiconfig/ombiconfig.component.html b/src/Ombi/ClientApp/src/app/wizard/ombiconfig/ombiconfig.component.html new file mode 100644 index 000000000..33d15bb58 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/ombiconfig/ombiconfig.component.html @@ -0,0 +1,28 @@ +
    +
    + +
    +
    +
    +

    Customize your Ombi

    +

    Customize your {{config.applicationName}}

    +
    + + + +
    +
    + + + +
    +
    + + Custom Logo + + +
    +
    +
    +
    + diff --git a/src/Ombi/ClientApp/src/app/wizard/ombiconfig/ombiconfig.component.ts b/src/Ombi/ClientApp/src/app/wizard/ombiconfig/ombiconfig.component.ts new file mode 100644 index 000000000..0b7e3863b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/ombiconfig/ombiconfig.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from "@angular/core"; +import { IOmbiConfigModel } from "../models/OmbiConfigModel"; +import { WizardService } from "../services/wizard.service"; + +@Component({ + selector: "wizard-ombi", + templateUrl: "./ombiconfig.component.html", + styleUrls: ["../welcome/welcome.component.scss"] +}) +export class OmbiConfigComponent { + + @Input() public config: IOmbiConfigModel; +} diff --git a/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.html b/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.html index 4a120f790..611dc4500 100644 --- a/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.html @@ -1,24 +1,38 @@ -
    -
    - - - -
    -
    - - - -
    -
    -Please note we do not store this information, we only store your Plex Authorization Token that will allow Ombi to view your media and users -
    -
    - +
    +
    +
    -
    -

    OR

    -
    -
    - +
    +
    +

    Plex Configuration

    +
    +
    + + + +
    +
    + + + +
    +
    + Please note we do not store this information, we only store your Plex Authorization Token that will allow Ombi to view your media and users +
    +
    +
    + +
    +
    +

    OR

    +
    +
    + +
    +
    +
    +
    diff --git a/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.ts b/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.ts index e469ddc00..bc80c65cb 100644 --- a/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/plex/plex.component.ts @@ -9,12 +9,15 @@ import { StorageService } from "../../shared/storage/storage-service"; @Component({ selector: "wizard-plex", templateUrl: "./plex.component.html", + styleUrls: ["../welcome/welcome.component.scss"], }) export class PlexComponent implements OnInit, OnDestroy { public login: string; public password: string; public pinTimer: any; + public completed: boolean; + public oauthLoading: boolean; private clientId: string; @@ -42,7 +45,7 @@ export class PlexComponent implements OnInit, OnDestroy { usePlexAdminAccount: true, }).subscribe(y => { if (y.result) { - this.router.navigate(["login"]); + this.notificationService.success("Created your Plex User!"); } else { this.notificationService.error("Could not get the Plex Admin Information"); if (y.errors.length > 0) { @@ -56,6 +59,7 @@ export class PlexComponent implements OnInit, OnDestroy { } public oauth() { + this.oauthLoading = true; const oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0, location=0, status=0, @@ -68,20 +72,24 @@ export class PlexComponent implements OnInit, OnDestroy { this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => { oAuthWindow!.location.replace(x.url); - this.pinTimer = setInterval(() => { // this.notify.info("Authenticating", "Loading... Please Wait"); this.getPinResult(x.pinId); - }, 10000); + }, 3000); }); }); } public getPinResult(pinId: number) { this.plexOauth.oAuth(pinId).subscribe(x => { + if (!x.accessToken) { + if(!x.success) { + this.oauthLoading = false; + clearInterval(this.pinTimer); + this.notificationService.error(`Error From Plex: ${x.error}`) + } return; - // RETURN } this.identityService.createWizardUser({ @@ -92,10 +100,14 @@ export class PlexComponent implements OnInit, OnDestroy { if (u.result) { this.authService.oAuth(pinId).subscribe(c => { this.store.save("id_token", c.access_token); - this.router.navigate(["login"]); + this.completed = true; + this.notificationService.success("Created your Plex User!"); + this.oauthLoading = false; + clearInterval(this.pinTimer); }); } else { + this.oauthLoading = false; if (u.errors.length > 0) { console.log(u.errors[0]); } diff --git a/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts b/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts new file mode 100644 index 000000000..0f6511265 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts @@ -0,0 +1,19 @@ +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 { ICustomizationSettings } from "../../interfaces"; +import { ServiceHelpers } from "../../services"; +import { IOmbiConfigModel } from "../models/OmbiConfigModel"; + + +@Injectable() +export class WizardService extends ServiceHelpers { + constructor(public http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v2/wizard/", href); + } + + public addOmbiConfig(config: IOmbiConfigModel): Observable { + return this.http.post(`${this.url}config`, config, {headers: this.headers}); + } +} diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html index 416af7b09..c1189051c 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html @@ -1,56 +1,86 @@ -
    - +
    +
    Welcome -

    Welcome to Ombi, this wizard will quickly take you through the inital setup!

    -
    - + +
    +
    -
    - Plex - -
    - - -
    -
    -
    - - -
    - Emby - -
    - - -
    -
    -
    - - -
    - Create a local admin - -
    - - -
    -
    -
    +
    + Media Server + + + + + + + +
    + + +
    + Create a local admin + +
    + + +
    +
    +
    + +
    + Ombi config + +
    + + +
    +
    +
    Done - All setup! Press Finish to continue and login to Ombi! +
    +
    + +
    +
    +
    +

    All setup!

    + Press Finish to continue and login to Ombi! +
    +
    +
    - - + +
    -
    +
    +
    \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss new file mode 100644 index 000000000..053516b6f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss @@ -0,0 +1,172 @@ +@import "~styles/variables.scss"; +.welcome-container{ + display:flex; + height:100%; + width:100%; +} + +.welcome-container .logo{ + font-family: Montserrat,sans-serif; + text-transform: uppercase; + color: #62d2fa; + font-weight: 700; + font-size: 5em; + display:flex; + justify-content: center; + align-items: center; + height:300px; +} + +.left-container{ + width:30%; +} + +.right-container{ + width:60%; + float:right; +} + +.right-container-content{ + width:100%; +} + +.welcome-container .right-container{ + display:flex; + align-items: center; + justify-content: center; +} + +.welcome-buttons{ + float:right; +} + +.fab:before { + font-family: 'Font Awesome 5 Brands'; +} + +.social-media .fab{ + font-family: Roboto, "Helvetica Neue", sans-serif; +} + +.fas:before { + font-family:"Font Awesome 5 Free"; + font-weight:800; +} + +.social-media .fas{ + font-family: Roboto, "Helvetica Neue", sans-serif; + font-weight:400; +} + +.social-media a{ + color: #FFF; +} + +.social-media{ + display:grid; +} + +.fa-ul { + list-style-type: none; + margin-left: 2.5em; + padding-left: 0; +} + +.fa-li { + left: -2em; + position: absolute; + text-align: center; + width: 2em; + line-height: inherit; +} + +.social-media ul li{ + font-size:16px; + padding:5px; +} + + +.left-container.mediaserver img{ + object-fit: contain; + width: 100%; +} + +.left-container.mediaserver{ + display:flex; + justify-content: center; + align-items: center; + height:300px; + padding:20px; +} + +.right-container.mediaserver{ + display:flex; + justify-content: center; + align-items:center; +} + +.right-container-content .mediaserver{ + display:block; +} + +.mediaserver-container{ + display:flex; + height:100%; + width:100%; + justify-content: space-between; +} + +.plex-buttons{ + display:flex; + justify-content: center; + align-items: center; +} + +p.space-or{ + padding:20px; + margin-top:20px; +} + +.viewon-btn.plex { + border: 1px solid #e5a00d; + color: #e5a00d; +} + +.viewon-btn { + background-color: transparent; + text-decoration: none; +} + +.viewon-btn.standard { + border: 1px solid #FFF; +} + +.viewon-btn.emby { + border: 1px solid #52B54B; + color: #52B54B; +} + +.viewon-btn.jellyfin { + border: 1px solid #A45FC4; + color: #A45FC4; +} + +.text-logo{ + font-size:12em; +} + +.left{ + float:left; +} + +.right{ + float:right; +} + +small.important{ + color:red; +} + +h1.wizard-title{ + margin-top:30px; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts index a46b04bd4..849977b55 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts @@ -1,35 +1,53 @@ -import { Component, OnInit } from "@angular/core"; +import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; import { ICreateWizardUser } from "../../interfaces"; import { IdentityService, NotificationService } from "../../services"; +import { IOmbiConfigModel } from "../models/OmbiConfigModel"; +import { WizardService } from "../services/wizard.service"; +import { MatHorizontalStepper } from'@angular/material/stepper'; +import { StepperSelectionEvent } from "@angular/cdk/stepper"; @Component({ templateUrl: "./welcome.component.html", + styleUrls: ["./welcome.component.scss"], }) export class WelcomeComponent implements OnInit { - + + @ViewChild('stepper', {static: false}) public stepper: MatHorizontalStepper; public localUser: ICreateWizardUser; - - constructor(private router: Router, - private identityService: IdentityService, private notificationService: NotificationService) { } + public config: IOmbiConfigModel; + + constructor(private router: Router, private identityService: IdentityService, + private notificationService: NotificationService, private WizardService: WizardService) { } - public ngOnInit(): void { + public ngOnInit(): void { this.localUser = { password:"", username:"", usePlexAdminAccount:false } + this.config = { + applicationName: null, + applicationUrl: null, + logo: null + }; } public createUser() { - this.identityService.createWizardUser(this.localUser).subscribe(x => { - if (x.result) { + this.WizardService.addOmbiConfig(this.config).subscribe(config => { + if(config != null) { + this.identityService.createWizardUser(this.localUser).subscribe(x => { + if (x.result) { + // save the config this.router.navigate(["login"]); } else { if (x.errors.length > 0) { this.notificationService.error(x.errors[0]); + this.stepper.previous(); } } }); } + }, configErr => this.notificationService.error(configErr)); + } } diff --git a/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts b/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts index 5438e8e9c..501995ce6 100644 --- a/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts +++ b/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts @@ -7,14 +7,18 @@ import { MatStepperModule } from "@angular/material/stepper"; import { CreateAdminComponent } from "./createadmin/createadmin.component"; import { EmbyComponent } from "./emby/emby.component"; +import { JellyfinComponent } from "./jellyfin/jellyfin.component"; import { MediaServerComponent } from "./mediaserver/mediaserver.component"; import { PlexComponent } from "./plex/plex.component"; import { WelcomeComponent } from "./welcome/welcome.component"; +import { OmbiConfigComponent } from "./ombiconfig/ombiconfig.component"; import { EmbyService } from "../services"; +import { JellyfinService } from "../services"; import { PlexService } from "../services"; import { IdentityService } from "../services"; import { PlexOAuthService } from "../services"; +import { WizardService } from "./services/wizard.service"; import { SharedModule } from "../shared/shared.module"; @@ -23,7 +27,9 @@ const routes: Routes = [ { path: "MediaServer", component: MediaServerComponent}, { path: "Plex", component: PlexComponent}, { path: "Emby", component: EmbyComponent}, + { path: "Jellyfin", component: JellyfinComponent}, { path: "CreateAdmin", component: CreateAdminComponent}, + { path: "OmbiConfig", component: OmbiConfigComponent}, ]; @NgModule({ imports: [ @@ -40,6 +46,8 @@ const routes: Routes = [ PlexComponent, CreateAdminComponent, EmbyComponent, + JellyfinComponent, + OmbiConfigComponent, ], exports: [ RouterModule, @@ -48,7 +56,9 @@ const routes: Routes = [ PlexService, IdentityService, EmbyService, + JellyfinService, PlexOAuthService, + WizardService, ], }) diff --git a/src/Ombi/ClientApp/src/index.html b/src/Ombi/ClientApp/src/index.html index b71541a3e..57d663a05 100644 --- a/src/Ombi/ClientApp/src/index.html +++ b/src/Ombi/ClientApp/src/index.html @@ -25,7 +25,7 @@ - +