From 9fe644f3dbb2a36087de86cbef2f8717e55cce06 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 10 Dec 2020 01:33:55 -0500 Subject: [PATCH 01/21] Add separate Jellyfin server type Due to forthcoming changes to the Jellyfin API, this adds support for Jellyfin as server type completely independent from Emby. It also undoes the workarounds that treated Jellyfin as a subset of Emby. --- src/Ombi.Api.Emby/EmbyApiFactory.cs | 4 - src/Ombi.Api.Emby/Models/PublicInfo.cs | 8 +- src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs | 33 ++ src/Ombi.Api.Jellyfin/IJellyfinApi.cs | 10 + .../JellyfinApi.cs | 52 +-- src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs | 37 +++ .../Models/JellyfinConfiguration.cs | 45 +++ .../Models/JellyfinConnectUser.cs | 47 +++ .../Models/JellyfinItemContainer.cs | 10 + .../Models/JellyfinMediaType.cs | 10 + .../Models/JellyfinPolicy.cs | 59 ++++ .../Models/JellyfinSystemInfo.cs | 63 ++++ src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs | 47 +++ .../Models/JellyfinUserLogin.cs | 7 + .../Models/Media/JellyfinChapter.cs | 8 + .../Models/Media/JellyfinExternalurl.cs | 8 + .../Models/Media/JellyfinImagetags.cs | 11 + .../Models/Media/JellyfinMediasource.cs | 30 ++ .../Models/Media/JellyfinMediastream.cs | 36 +++ .../Models/Media/JellyfinPerson.cs | 11 + .../Models/Media/JellyfinProviderids.cs | 13 + .../Models/Media/JellyfinRemotetrailer.cs | 8 + .../Models/Media/JellyfinStudio.cs | 8 + .../Models/Media/JellyfinUserdata.cs | 15 + .../Models/Media/Movie/JellyfinMovie.cs | 34 ++ .../Models/Media/Movie/MovieInformation.cs | 60 ++++ .../Models/Media/Tv/EpisodeInformation.cs | 71 +++++ .../Models/Media/Tv/JellyfinEpisodes.cs | 45 +++ .../Models/Media/Tv/JellyfinRemotetrailer.cs | 8 + .../Models/Media/Tv/JellyfinSeries.cs | 32 ++ .../Media/Tv/JellyfinSeriesstudioinfo.cs | 8 + .../Models/Media/Tv/SeriesInformation.cs | 59 ++++ src/Ombi.Api.Jellyfin/Models/PublicInfo.cs | 19 ++ .../Ombi.Api.Jellyfin.csproj | 17 + .../Rule/Search/EmbyAvailabilityRuleTests.cs | 105 +++++- .../Authentication/OmbiUserManager.cs | 46 ++- src/Ombi.Core/Engine/RecentlyAddedEngine.cs | 98 +++++- .../Models/RecentlyAddedMovieModel.cs | 5 +- .../Models/Search/SearchViewModel.cs | 5 +- src/Ombi.Core/Models/UserDto.cs | 5 +- src/Ombi.Core/Ombi.Core.csproj | 3 +- .../Rules/Search/AvailabilityRuleHelper.cs | 32 +- .../Rules/Search/JellyfinAvailabilityRule.cs | 103 ++++++ src/Ombi.DependencyInjection/IocExtensions.cs | 10 +- .../Checks/CouchPotatoHealthCheck.cs | 2 + .../Checks/JellyfinHealthCheck.cs | 54 ++++ .../Checks/SickrageHealthCheck.cs | 2 + .../HealthCheckExtensions.cs | 1 + .../Ombi.HealthChecks.csproj | 1 + src/Ombi.Helpers/JellyfinHelper.cs | 27 ++ src/Ombi.Helpers/LoggingEvents.cs | 4 +- .../Jellyfin/IJellyfinAvaliabilityChecker.cs | 8 + .../Jobs/Jellyfin/IJellyfinContentSync.cs | 8 + .../Jobs/Jellyfin/IJellyfinEpisodeSync.cs | 8 + .../Jobs/Jellyfin/IJellyfinUserImporter.cs | 8 + .../Jellyfin/JellyfinAvaliabilityChecker.cs | 235 ++++++++++++++ .../Jobs/Jellyfin/JellyfinContentSync.cs | 231 ++++++++++++++ .../Jobs/Jellyfin/JellyfinEpisodeSync.cs | 181 +++++++++++ .../Jobs/Jellyfin/JellyfinUserImporter.cs | 173 ++++++++++ .../Jobs/Ombi/MediaDatabaseRefresh.cs | 30 +- src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs | 299 +++++++++++++++++- .../Jobs/Ombi/RefreshMetadata.cs | 123 ++++++- src/Ombi.Schedule/Ombi.Schedule.csproj | 3 +- src/Ombi.Schedule/OmbiScheduler.cs | 13 +- .../Models/External/JellyfinSettings.cs | 22 ++ .../Settings/Models/JobSettings.cs | 3 +- .../Settings/Models/JobSettingsHelper.cs | 7 +- .../Settings/Models/UserManagementSettings.cs | 4 +- src/Ombi.Store/Context/ExternalContext.cs | 10 +- src/Ombi.Store/Entities/JellyfinContent.cs | 71 +++++ src/Ombi.Store/Entities/JellyfinEpisode.cs | 53 ++++ src/Ombi.Store/Entities/RecentlyAddedLog.cs | 5 +- src/Ombi.Store/Entities/User.cs | 3 +- .../Repository/IJellyfinContentRepository.cs | 24 ++ .../Repository/JellyfinContentRepository.cs | 106 +++++++ src/Ombi.sln | 2 + .../card/discover-card-details.component.html | 13 +- .../grid/discover-grid.component.html | 13 +- .../src/app/interfaces/IRecentlyAdded.ts | 1 + .../src/app/interfaces/ISearchMovieResult.ts | 1 + .../app/interfaces/ISearchMovieResultV2.ts | 3 +- .../src/app/interfaces/ISearchTvResult.ts | 1 + .../src/app/interfaces/ISearchTvResultV2.ts | 2 + .../ClientApp/src/app/interfaces/ISettings.ts | 22 ++ .../ClientApp/src/app/interfaces/IUser.ts | 1 + .../movie/movie-details.component.html | 4 +- .../social-icons/social-icons.component.html | 7 +- .../social-icons/social-icons.component.scss | 6 +- .../social-icons/social-icons.component.ts | 1 + .../components/tv/tv-details.component.html | 4 +- .../src/app/search/moviesearch.component.html | 3 + .../src/app/search/tvsearch.component.html | 5 + .../src/app/services/applications/index.ts | 1 + .../services/applications/jellyfin.service.ts | 28 ++ .../services/applications/tester.service.ts | 5 + .../ClientApp/src/app/services/job.service.ts | 8 + .../src/app/services/settings.service.ts | 9 + .../src/app/settings/emby/emby.component.ts | 1 - .../settings/jellyfin/jellyfin.component.html | 111 +++++++ .../settings/jellyfin/jellyfin.component.scss | 41 +++ .../settings/jellyfin/jellyfin.component.ts | 95 ++++++ .../src/app/settings/jobs/jobs.component.html | 7 + .../src/app/settings/jobs/jobs.component.ts | 1 + .../src/app/settings/settings.module.ts | 6 +- .../app/settings/settingsmenu.component.html | 5 +- .../usermanagement.component.ts | 33 +- .../usermanagement.component.html | 4 +- .../wizard/jellyfin/jellyfin.component.html | 31 ++ .../app/wizard/jellyfin/jellyfin.component.ts | 52 +++ .../mediaserver/mediaserver.component.html | 9 +- .../app/wizard/welcome/welcome.component.html | 11 + .../ClientApp/src/app/wizard/wizard.module.ts | 5 + .../V1/External/JellyfinController.cs | 96 ++++++ src/Ombi/Controllers/V1/JobController.cs | 20 +- src/Ombi/Controllers/V1/SettingsController.cs | 42 +++ src/Ombi/Ombi.csproj | 1 + 116 files changed, 3672 insertions(+), 103 deletions(-) create mode 100644 src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs create mode 100644 src/Ombi.Api.Jellyfin/IJellyfinApi.cs rename src/{Ombi.Api.Emby => Ombi.Api.Jellyfin}/JellyfinApi.cs (65%) create mode 100644 src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs create mode 100644 src/Ombi.Api.Jellyfin/Models/PublicInfo.cs create mode 100644 src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj create mode 100644 src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs create mode 100644 src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs create mode 100644 src/Ombi.Helpers/JellyfinHelper.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs create mode 100644 src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs create mode 100644 src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs create mode 100644 src/Ombi.Store/Entities/JellyfinContent.cs create mode 100644 src/Ombi.Store/Entities/JellyfinEpisode.cs create mode 100644 src/Ombi.Store/Repository/IJellyfinContentRepository.cs create mode 100644 src/Ombi.Store/Repository/JellyfinContentRepository.cs create mode 100644 src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts create mode 100644 src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html create mode 100644 src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.scss create mode 100644 src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts create mode 100644 src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html create mode 100644 src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts create mode 100644 src/Ombi/Controllers/V1/External/JellyfinController.cs 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..75be0f172 100644 --- a/src/Ombi.Api.Emby/Models/PublicInfo.cs +++ b/src/Ombi.Api.Emby/Models/PublicInfo.cs @@ -5,15 +5,9 @@ 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 65% rename from src/Ombi.Api.Emby/JellyfinApi.cs rename to src/Ombi.Api.Jellyfin/JellyfinApi.cs index 197fba684..1265791d0 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 @@ -67,15 +67,15 @@ namespace Ombi.Api.Emby request.AddJsonBody(body); - request.AddHeader("X-Emby-Authorization", + request.AddHeader("X-Jellyfin-Authorization", $"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..d1a87ddbe --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs @@ -0,0 +1,11 @@ +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; } + } +} \ No newline at end of file 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..56ae605e2 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs @@ -0,0 +1,19 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public class PublicInfo + { + 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/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.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs index 8e5c57d67..ed3e60631 100644 --- a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs @@ -115,4 +115,107 @@ namespace Ombi.Core.Tests.Rule.Search Assert.False(search.Available); } } -} \ No newline at end of file + + 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#!/item?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 + { + 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/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/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs index 114d32a24..d721cc768 100644 --- a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -13,38 +13,43 @@ 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 +57,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 +143,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 +202,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 +228,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 +307,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/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/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/UserDto.cs b/src/Ombi.Core/Models/UserDto.cs index 5c629fb39..6a8155bdb 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 = 4 } -} \ 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/JellyfinAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs new file mode 100644 index 000000000..c900844a3 --- /dev/null +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs @@ -0,0 +1,103 @@ +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, s.IsJellyfin); + } + else + { + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, null, s.IsJellyfin); + } + } + + 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..7571cdca4 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; @@ -126,6 +128,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -153,8 +156,8 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) { @@ -169,6 +172,7 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -213,6 +217,9 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -220,6 +227,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); 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/JellyfinHelper.cs b/src/Ombi.Helpers/JellyfinHelper.cs new file mode 100644 index 000000000..7a342160e --- /dev/null +++ b/src/Ombi.Helpers/JellyfinHelper.cs @@ -0,0 +1,27 @@ +namespace Ombi.Helpers +{ + public static class JellyfinHelper + { + public static string GetJellyfinMediaUrl(string mediaId, string serverId, string customerServerUrl = null, bool isJellyfin = false) + { + //web/index.html#!/details|item + string path = "item"; + if (isJellyfin) + { + 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 $"https://app.jellyfin.media/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.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..2453f2008 --- /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 {1} for server {0}", server.Name, jellyfinSettings.IsJellyfin ? "Jellyfin" : "Jellyfin"); + } + } + + 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, settings.IsJellyfin), + 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); + } + } + +} \ No newline at end of file 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..2c39616ff --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs @@ -0,0 +1,173 @@ +#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, $"{(settings.IsJellyfin ? "Jellyfin" : "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.ConnectUserName.HasValue() && !jellyfinUser.Name.HasValue()) + { + _log.LogInformation("Could not create Jellyfin user since the have no username, PlexUserId: {0}", jellyfinUser.Id); + continue; + } + var isConnectUser = jellyfinUser.ConnectUserName.HasValue(); + // Create this users + var newUser = new OmbiUser + { + UserName = jellyfinUser.Name, + ProviderUserId = jellyfinUser.Id, + Alias = isConnectUser ? jellyfinUser.Name : string.Empty, + MovieRequestLimit = userManagementSettings.MovieRequestLimit, + EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit + }; + 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/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index 66ce28487..1cc0a9cfd 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -5,6 +5,7 @@ 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 +15,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 +29,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 +37,7 @@ namespace Ombi.Schedule.Jobs.Ombi { await RemovePlexData(); await RemoveEmbyData(); + await RemoveJellyfinData(); } catch (Exception e) { @@ -64,6 +68,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 @@ -108,4 +134,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..a3db357af 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) @@ -379,6 +441,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 +496,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 +523,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 +532,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 +552,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 +564,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 +583,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 +743,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 +1139,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 +1321,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 30878adaf..f04cf5743 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); + 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); + 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); @@ -383,4 +500,4 @@ namespace Ombi.Schedule.Jobs.Ombi GC.SuppressFinalize(this); } } -} \ No newline at end of file +} 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/JellyfinSettings.cs b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs new file mode 100644 index 000000000..0785137c5 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs @@ -0,0 +1,22 @@ +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 bool IsJellyfin { 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..7d0271722 100644 --- a/src/Ombi.Settings/Settings/Models/UserManagementSettings.cs +++ b/src/Ombi.Settings/Settings/Models/UserManagementSettings.cs @@ -7,10 +7,12 @@ 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 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/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/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/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/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.sln b/src/Ombi.sln index 7a32a4bf9..29decbc1c 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", "{08FF107D-31E1-470D-AF86-E09B015CEE06}" +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}" 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..d7bac2d96 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 @@ -22,6 +22,10 @@ + + + @@ -33,6 +37,10 @@ + + + @@ -151,10 +159,13 @@ {{'Search.ViewOnEmby' | translate}} + {{'Search.ViewOnJellyfin' | + translate}} - \ No newline at end of file + 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..e413cc372 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 + - \ 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/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..42ce7716f 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; 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..bcd0e7d4a 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts @@ -52,7 +52,26 @@ export interface IEmbyServer extends IExternalSettings { export interface IPublicInfo { id: string; serverName: string; +} + +export interface IJellyfinSettings extends ISettings { + enable: boolean; isJellyfin: 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 +160,7 @@ export interface ICustomizationSettings extends ISettings { export interface IJobSettings { embyContentSync: string; + jellyfinContentSync: string; sonarrSync: string; radarrSync: string; plexContentSync: string; @@ -187,11 +207,13 @@ 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[]; } export interface IAbout { diff --git a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts index 4235d55cf..30f4837d2 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts @@ -48,6 +48,7 @@ export enum UserType { LocalUser = 1, PlexUser = 2, EmbyUser = 3, + JellyfinUser = 4, } export interface IIdentityResult { 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..0f057b6a4 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 @@ -17,7 +17,7 @@
+ [instagram]="movie.externalIds.instagramId" [available]="movie.available" [plexUrl]="movie.plexUrl" [embyUrl]="movie.embyUrl" [jellyfinUrl]="movie.jellyfinUrl">
@@ -198,4 +198,4 @@ - \ No newline at end of file + 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..de923204a 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 @@ -40,4 +40,9 @@ class="fa fa-play-circle fa-2x grow-social"> - \ No newline at end of file + + + + + diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.scss b/src/Ombi/ClientApp/src/app/media-details/components/shared/social-icons/social-icons.component.scss index 866723b93..67d621a33 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 @@ -6,4 +6,8 @@ .media-icons.emby { color: #52b54a !important; -} \ No newline at end of file +} + +.media-icons.jellyfin { + color: #00a4dc !important; +} 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..882bf2177 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 @@ -18,6 +18,7 @@ export class SocialIconsComponent { @Input() available: boolean; @Input() plexUrl: string; @Input() embyUrl: string; + @Input() jellyfinUrl: string; @Input() doNotAppend: boolean; @Output() openTrailer: EventEmitter = new EventEmitter(); 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..fd89daa41 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 @@ -24,7 +24,7 @@
- +
@@ -125,4 +125,4 @@ - \ No newline at end of file + diff --git a/src/Ombi/ClientApp/src/app/search/moviesearch.component.html b/src/Ombi/ClientApp/src/app/search/moviesearch.component.html index 30507d004..dd95d8a74 100644 --- a/src/Ombi/ClientApp/src/app/search/moviesearch.component.html +++ b/src/Ombi/ClientApp/src/app/search/moviesearch.component.html @@ -168,6 +168,9 @@ {{'Search.ViewOnEmby' | translate}} + {{'Search.ViewOnJellyfin' | + translate}} + +
+
+
+
+ + Server Name + + +
+ +
+ + Hostname / IP + + + +
+ + Server ID + + +
+ + Port + + + + + SSL + +
+
+ + API Key + + +
+
+ + Base URL + + +
+
+ + Externally Facing Hostname + + + + Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1" + Current URL: "https://app.jellyfin.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1 + +
+ + +
+
+ +
+
+
+
+ +
+
+
+ + + + + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+ + + 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..2ae0d331d --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts @@ -0,0 +1,95 @@ +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"); + } + }); + } +} 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 ec2a42c99..982643e2e 100644 --- a/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.html +++ b/src/Ombi/ClientApp/src/app/settings/jobs/jobs.component.html @@ -78,6 +78,13 @@ +
+ + + The Jellyfin Sync is required + +
+
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/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..ce97ee200 100644 --- a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html @@ -14,7 +14,8 @@ - + + @@ -61,4 +62,4 @@ - \ 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..77447ce21 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,6 +24,10 @@ export class UserManagementComponent implements OnInit { public filteredEmbyUsers: IUsersModel[]; public bannedEmbyUsers: IUsersModel[] = []; + public jellyfinUsers: IUsersModel[]; + public filteredJellyfinUsers: IUsersModel[]; + public bannedJellyfinUsers: IUsersModel[] = []; + public enableImportButton = false; constructor(private readonly settingsService: SettingsService, @@ -30,14 +35,15 @@ export class UserManagementComponent implements OnInit { 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.settingsService.getUserManagementSettings().subscribe(x => { this.settings = x; - if(x.importEmbyUsers || x.importPlexUsers) { + if(x.importEmbyUsers || x.importJellyfinUsers || x.importPlexUsers) { this.enableImportButton = true; } @@ -65,6 +71,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 +98,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 +108,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 +131,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/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html index 4b64e2f45..9490452ae 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html @@ -87,7 +87,9 @@ Local User Plex User - Emby User + Emby User + Jellyfin User + 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..38c220b04 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html @@ -0,0 +1,31 @@ + +
+
+
+
+ + + +
+ +
+ + + + +
+
+ Enable SSL +
+
+ + + + +
+
+ Save
+
+
+
+
\ No newline at end of file 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..4c42c88ac --- /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", +}) +export class JellyfinComponent implements OnInit { + + public jellyfinSettings: IJellyfinSettings; + + constructor(private jellyfinService: JellyfinService, + private notificationService: NotificationService) { + } + + public ngOnInit() { + this.jellyfinSettings = { + servers: [], + isJellyfin: false, + 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/welcome/welcome.component.html b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html index 416af7b09..dfec7f8d9 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html @@ -33,6 +33,17 @@ + +
+ Jellyfin + +
+ + +
+
+
+
Create a local admin diff --git a/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts b/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts index 5438e8e9c..1e34a1de9 100644 --- a/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts +++ b/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts @@ -7,11 +7,13 @@ 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 { EmbyService } from "../services"; +import { JellyfinService } from "../services"; import { PlexService } from "../services"; import { IdentityService } from "../services"; import { PlexOAuthService } from "../services"; @@ -23,6 +25,7 @@ const routes: Routes = [ { path: "MediaServer", component: MediaServerComponent}, { path: "Plex", component: PlexComponent}, { path: "Emby", component: EmbyComponent}, + { path: "Jellyfin", component: JellyfinComponent}, { path: "CreateAdmin", component: CreateAdminComponent}, ]; @NgModule({ @@ -40,6 +43,7 @@ const routes: Routes = [ PlexComponent, CreateAdminComponent, EmbyComponent, + JellyfinComponent, ], exports: [ RouterModule, @@ -48,6 +52,7 @@ const routes: Routes = [ PlexService, IdentityService, EmbyService, + JellyfinService, PlexOAuthService, ], diff --git a/src/Ombi/Controllers/V1/External/JellyfinController.cs b/src/Ombi/Controllers/V1/External/JellyfinController.cs new file mode 100644 index 000000000..27663a5b6 --- /dev/null +++ b/src/Ombi/Controllers/V1/External/JellyfinController.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; +using Ombi.Api.Plex; +using Ombi.Attributes; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Models.External; + +namespace Ombi.Controllers.V1.External +{ + [Admin] + [ApiV1] + [Produces("application/json")] + public class JellyfinController : Controller + { + + public JellyfinController(IJellyfinApiFactory jellyfin, ISettingsService jellyfinSettings) + { + JellyfinApi = jellyfin; + JellyfinSettings = jellyfinSettings; + } + + private IJellyfinApiFactory JellyfinApi { get; } + private ISettingsService JellyfinSettings { get; } + + /// + /// Signs into the Jellyfin Api + /// + /// The request. + /// + [HttpPost] + [AllowAnonymous] + public async Task SignIn([FromBody] JellyfinSettings request) + { + // Check if settings exist since we allow anon... + var settings = await JellyfinSettings.GetSettingsAsync(); + if (settings?.Servers?.Any() ?? false) return null; + + var client = await JellyfinApi.CreateClient(); + request.Enable = true; + var firstServer = request.Servers.FirstOrDefault(); + // Test that we can connect + var result = await client.GetUsers(firstServer.FullUri, firstServer.ApiKey); + + if (result != null && result.Any()) + { + firstServer.AdministratorId = result.FirstOrDefault(x => x.Policy.IsAdministrator)?.Id ?? string.Empty; + await JellyfinSettings.SaveSettingsAsync(request); + + return request; + } + return null; + } + + [HttpPost("info")] + public async Task GetServerInfo([FromBody] JellyfinServers server) + { + var client = await JellyfinApi.CreateClient(); + var result = await client.GetPublicInformation(server.FullUri); + return result; + } + + /// + /// Gets the jellyfin users. + /// + /// + [HttpGet("users")] + public async Task> JellyfinUsers() + { + var vm = new List(); + var s = await JellyfinSettings.GetSettingsAsync(); + var client = JellyfinApi.CreateClient(s); + foreach (var server in s?.Servers ?? new List()) + { + var users = await client.GetUsers(server.FullUri, server.ApiKey); + if (users != null && users.Any()) + { + vm.AddRange(users.Select(u => new UsersViewModel + { + Username = u.Name, + Id = u.Id + })); + } + } + + // Filter out any dupes + return vm.DistinctBy(x => x.Id); + } + } +} diff --git a/src/Ombi/Controllers/V1/JobController.cs b/src/Ombi/Controllers/V1/JobController.cs index 6ffbf8486..7f05474dc 100644 --- a/src/Ombi/Controllers/V1/JobController.cs +++ b/src/Ombi/Controllers/V1/JobController.cs @@ -6,6 +6,7 @@ using Ombi.Attributes; using Ombi.Helpers; using Ombi.Schedule.Jobs; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Radarr; @@ -92,13 +93,13 @@ namespace Ombi.Controllers.V1 } /// - /// Runs the Emby User importer + /// Runs the Jellyfin User importer /// /// [HttpPost("embyuserimporter")] - public async Task EmbyUserImporter() + public async Task JellyfinUserImporter() { - await OmbiQuartz.TriggerJob(nameof(IEmbyUserImporter), "Emby"); + await OmbiQuartz.TriggerJob(nameof(IJellyfinUserImporter), "Jellyfin"); return true; } @@ -135,6 +136,17 @@ namespace Ombi.Controllers.V1 return true; } + /// + /// Runs the Jellyfin Content Cacher + /// + /// + [HttpPost("jellyfincontentcacher")] + public async Task StartJellyfinContentCacher() + { + await OmbiQuartz.TriggerJob(nameof(IJellyfinContentSync), "Jellyfin"); + return true; + } + /// /// Runs the Arr Availability Checker /// @@ -165,4 +177,4 @@ namespace Ombi.Controllers.V1 return true; } } -} \ No newline at end of file +} diff --git a/src/Ombi/Controllers/V1/SettingsController.cs b/src/Ombi/Controllers/V1/SettingsController.cs index 1d340b4cd..9e7343472 100644 --- a/src/Ombi/Controllers/V1/SettingsController.cs +++ b/src/Ombi/Controllers/V1/SettingsController.cs @@ -10,6 +10,7 @@ using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.Github; using Ombi.Attributes; using Ombi.Core.Engine; @@ -29,6 +30,7 @@ using Ombi.Extensions; using Quartz; using Ombi.Schedule.Jobs; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Sonarr; using Ombi.Schedule.Jobs.Lidarr; @@ -48,6 +50,7 @@ namespace Ombi.Controllers.V1 IMapper mapper, INotificationTemplatesRepository templateRepo, IEmbyApiFactory embyApi, + IJellyfinApiFactory jellyfinApi, ICacheService memCache, IGithubApi githubApi, IRecentlyAddedEngine engine) @@ -56,6 +59,7 @@ namespace Ombi.Controllers.V1 Mapper = mapper; TemplateRepository = templateRepo; _embyApi = embyApi; + _jellyfinApi = jellyfinApi; _cache = memCache; _githubApi = githubApi; _recentlyAdded = engine; @@ -65,6 +69,7 @@ namespace Ombi.Controllers.V1 private IMapper Mapper { get; } private INotificationTemplatesRepository TemplateRepository { get; } private readonly IEmbyApiFactory _embyApi; + private readonly IJellyfinApiFactory _jellyfinApi; private readonly ICacheService _cache; private readonly IGithubApi _githubApi; private readonly IRecentlyAddedEngine _recentlyAdded; @@ -234,6 +239,42 @@ namespace Ombi.Controllers.V1 return result; } + /// + /// Gets the Jellyfin Settings. + /// + /// + [HttpGet("jellyfin")] + public async Task JellyfinSettings() + { + return await Get(); + } + + /// + /// Save the Jellyfin settings. + /// + /// The jellyfin. + /// + [HttpPost("jellyfin")] + public async Task JellyfinSettings([FromBody]JellyfinSettings jellyfin) + { + if (jellyfin.Enable) + { + var client = await _jellyfinApi.CreateClient(); + foreach (var server in jellyfin.Servers) + { + var users = await client.GetUsers(server.FullUri, server.ApiKey); + var admin = users.FirstOrDefault(x => x.Policy.IsAdministrator); + server.AdministratorId = admin?.Id; + } + } + var result = await Save(jellyfin); + if (result) + { + await OmbiQuartz.TriggerJob(nameof(IJellyfinContentSync), "Jellyfin"); + } + return result; + } + /// /// Gets the Landing Page Settings. /// @@ -566,6 +607,7 @@ namespace Ombi.Controllers.V1 j.AutomaticUpdater = j.AutomaticUpdater.HasValue() ? j.AutomaticUpdater : JobSettingsHelper.Updater(j); j.CouchPotatoSync = j.CouchPotatoSync.HasValue() ? j.CouchPotatoSync : JobSettingsHelper.CouchPotato(j); j.EmbyContentSync = j.EmbyContentSync.HasValue() ? j.EmbyContentSync : JobSettingsHelper.EmbyContent(j); + j.JellyfinContentSync = j.JellyfinContentSync.HasValue() ? j.JellyfinContentSync : JobSettingsHelper.JellyfinContent(j); j.PlexContentSync = j.PlexContentSync.HasValue() ? j.PlexContentSync : JobSettingsHelper.PlexContent(j); j.UserImporter = j.UserImporter.HasValue() ? j.UserImporter : JobSettingsHelper.UserImporter(j); j.SickRageSync = j.SickRageSync.HasValue() ? j.SickRageSync : JobSettingsHelper.SickRageSync(j); diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 9ac73264c..2c615f57c 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -71,6 +71,7 @@ + From 2f1471c61a3c215bd22783f8015dd0e7f298c0b1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 10 Dec 2020 10:32:56 -0500 Subject: [PATCH 02/21] Update Jellyfin authorization header We still use X-Emby-Authorization instead of X-Jellyfin-Authorization. Co-authored-by: Cody Robibero --- src/Ombi.Api.Jellyfin/JellyfinApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi.Api.Jellyfin/JellyfinApi.cs b/src/Ombi.Api.Jellyfin/JellyfinApi.cs index 1265791d0..a8d94eca6 100644 --- a/src/Ombi.Api.Jellyfin/JellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/JellyfinApi.cs @@ -67,7 +67,7 @@ namespace Ombi.Api.Jellyfin request.AddJsonBody(body); - request.AddHeader("X-Jellyfin-Authorization", + request.AddHeader("X-Emby-Authorization", $"MediaBrowser Client=\"Ombi\", Device=\"Ombi\", DeviceId=\"v3\", Version=\"v3\""); AddHeaders(request, apiKey); From b48c8f7592c3c50767ecd06af73a4b2c50bb7e5e Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 00:00:38 -0500 Subject: [PATCH 03/21] Add database migration to SQLite --- .../20201211042424_Jellyfin.Designer.cs | 1221 +++++++++++++++++ .../OmbiSqlite/20201211042424_Jellyfin.cs | 69 + 2 files changed, 1290 insertions(+) create mode 100644 src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs create mode 100644 src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.cs diff --git a/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs new file mode 100644 index 000000000..4fc5f9456 --- /dev/null +++ b/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs @@ -0,0 +1,1221 @@ +// +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("20201211042424_Jellyfin")] + partial class Jellyfin + { + 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("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/20201211042424_Jellyfin.cs b/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.cs new file mode 100644 index 000000000..4e9187237 --- /dev/null +++ b/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Ombi.Store.Migrations.OmbiSqlite +{ + 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), + AddedAt = table.Column(type: "TEXT", nullable: false), + JellyfinId = table.Column(type: "TEXT", nullable: false), + ProviderId = table.Column(type: "TEXT", nullable: true), + Title = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false) + }, + 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), + AddedAt = table.Column(type: "TEXT", nullable: false), + JellyfinId = table.Column(type: "TEXT", nullable: true), + EpisodeNumber = table.Column(type: "INTEGER", nullable: false), + ParentId = table.Column(type: "TEXT", nullable: true), + ProviderId = table.Column(type: "TEXT", nullable: true), + SeasonNumber = table.Column(type: "INTEGER", nullable: false), + Title = 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"); + } + } +} From ce9279a108a0990d4bd7618646c10b50dca31de4 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 19:29:07 -0500 Subject: [PATCH 04/21] Add missing field for Jellyfin users --- .../usermanagement/usermanagement.component.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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..bd259aff6 100644 --- a/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/src/app/settings/usermanagement/usermanagement.component.html @@ -35,6 +35,18 @@ +
+
+ Import Jellyfin Users +
+ +
+

Jellyfin Users excluded from Import

+ + +
+ +

Default Roles

@@ -74,4 +86,4 @@
- \ No newline at end of file + From 0812a3f780d0a6683a9e1cbb42d70f4490f4e8cf Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 20:38:07 -0500 Subject: [PATCH 05/21] Add more detailed migration documentation --- src/Ombi.Store/Migration.md | 50 ++++++++++++++++++++++++++++++++++++ src/Ombi.Store/Migration.txt | 3 --- 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/Ombi.Store/Migration.md delete mode 100644 src/Ombi.Store/Migration.txt diff --git a/src/Ombi.Store/Migration.md b/src/Ombi.Store/Migration.md new file mode 100644 index 000000000..5a8f9b667 --- /dev/null +++ b/src/Ombi.Store/Migration.md @@ -0,0 +1,50 @@ +``` +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. In `src/Ombi`, install the `Microsoft.EntityFrameworkCore.Design` package: + + ``` + cd src/Ombi + dotnet add package Microsoft.EntityFrameworkCore.Design + ``` + +1. For some reason, the `StartupSingleton.Instance.SecurityKey` in `src/Ombi/Extensions/StartupExtensions.cs` is invalid when running `otnet ef migrations add` so we must fix it; apply this patch which seems to do the job: + + ``` + @@ -79,7 +79,7 @@ namespace Ombi + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(StartupSingleton.Instance.SecurityKey)), + + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(StartupSingleton.Instance.SecurityKey + "s")), + RequireExpirationTime = true, + ValidateLifetime = true, + ValidAudience = "Ombi", + ``` + +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 From 0fdfab19ddeb535c25919f140f23c3baed3150fb Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 20:43:55 -0500 Subject: [PATCH 06/21] Move migration to proper database --- src/Ombi.Store/Migration.md | 2 +- .../20201212014227_Jellyfin.Designer.cs | 506 +++++++ .../20201212014227_Jellyfin.cs} | 2 +- .../ExternalSqliteContextModelSnapshot.cs | 354 +++-- .../20201211042424_Jellyfin.Designer.cs | 1221 ----------------- 5 files changed, 781 insertions(+), 1304 deletions(-) create mode 100644 src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.Designer.cs rename src/Ombi.Store/Migrations/{OmbiSqlite/20201211042424_Jellyfin.cs => ExternalSqlite/20201212014227_Jellyfin.cs} (98%) delete mode 100644 src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs diff --git a/src/Ombi.Store/Migration.md b/src/Ombi.Store/Migration.md index 5a8f9b667..8332e8ce9 100644 --- a/src/Ombi.Store/Migration.md +++ b/src/Ombi.Store/Migration.md @@ -21,7 +21,7 @@ If running migrations for any db provider other than Sqlite, then ensure the dat dotnet add package Microsoft.EntityFrameworkCore.Design ``` -1. For some reason, the `StartupSingleton.Instance.SecurityKey` in `src/Ombi/Extensions/StartupExtensions.cs` is invalid when running `otnet ef migrations add` so we must fix it; apply this patch which seems to do the job: +1. For some reason, the `StartupSingleton.Instance.SecurityKey` in `src/Ombi/Extensions/StartupExtensions.cs` is invalid when running `dotnet ef migrations add` so we must fix it; apply this patch which seems to do the job: ``` @@ -79,7 +79,7 @@ namespace Ombi 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/OmbiSqlite/20201211042424_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs similarity index 98% rename from src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.cs rename to src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs index 4e9187237..50d630b16 100644 --- a/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.cs +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -namespace Ombi.Store.Migrations.OmbiSqlite +namespace Ombi.Store.Migrations.ExternalSqlite { public partial class Jellyfin : Migration { 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/OmbiSqlite/20201211042424_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs deleted file mode 100644 index 4fc5f9456..000000000 --- a/src/Ombi.Store/Migrations/OmbiSqlite/20201211042424_Jellyfin.Designer.cs +++ /dev/null @@ -1,1221 +0,0 @@ -// -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("20201211042424_Jellyfin")] - partial class Jellyfin - { - 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("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 - } - } -} From 635079155f2998be2d5acfdfe0f4429236ac7fe3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 20:57:29 -0500 Subject: [PATCH 07/21] Correct legends with proper names --- src/Ombi/ClientApp/src/app/settings/emby/emby.component.html | 2 +- .../src/app/settings/jellyfin/jellyfin.component.html | 2 +- src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html | 4 ++-- .../ClientApp/src/app/wizard/jellyfin/jellyfin.component.html | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html index e6d35b1c1..c1d8c1a83 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html @@ -4,7 +4,7 @@
- Emby/Jellyfin Configuration + Emby Configuration
diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html index 00a0455e5..8eb1f90ed 100644 --- a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html @@ -4,7 +4,7 @@
- Jellyfin/Jellyfin Configuration + Jellyfin Configuration
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..9c92f8a86 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html @@ -4,7 +4,7 @@
- +
@@ -28,4 +28,4 @@
-
\ No newline at end of file +
diff --git a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html index 38c220b04..f88342c1d 100644 --- a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.html @@ -4,7 +4,7 @@
- +
@@ -28,4 +28,4 @@
- \ No newline at end of file + From f8b9b3ca259de62942ee16dc1a199ef55bfd6237 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 23:09:48 -0500 Subject: [PATCH 08/21] Add warning about reverting patch --- src/Ombi.Store/Migration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ombi.Store/Migration.md b/src/Ombi.Store/Migration.md index 8332e8ce9..50711820b 100644 --- a/src/Ombi.Store/Migration.md +++ b/src/Ombi.Store/Migration.md @@ -35,6 +35,8 @@ If running migrations for any db provider other than Sqlite, then ensure the dat ValidAudience = "Ombi", ``` + *WARNING*: Don't forget to undo this before building Ombi, or things will be broken! + 1. List the available `dbcontext`s, and select the one that matches the database your fields will go in: ``` From 6a96d6718b948ff759f00c8bc9e743d0e2fc1a23 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 11 Dec 2020 23:18:45 -0500 Subject: [PATCH 09/21] Remove remaining IsJellyfin sections --- src/Ombi.Api.Emby/Models/PublicInfo.cs | 1 - src/Ombi.Api.Jellyfin/Models/PublicInfo.cs | 9 +-------- .../Rule/Rules/Search/EmbyAvailabilityRule.cs | 6 +++--- .../Rule/Rules/Search/JellyfinAvailabilityRule.cs | 4 ++-- src/Ombi.Helpers/EmbyHelper.cs | 6 +----- src/Ombi.Helpers/JellyfinHelper.cs | 10 +++------- src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs | 6 +++--- src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs | 4 ++-- src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs | 6 +++--- .../Jobs/Jellyfin/JellyfinUserImporter.cs | 2 +- .../Settings/Models/External/EmbySettings.cs | 3 +-- .../Settings/Models/External/JellyfinSettings.cs | 1 - src/Ombi/ClientApp/src/app/interfaces/ISettings.ts | 2 -- .../src/app/settings/emby/emby.component.html | 4 ++-- .../src/app/settings/jellyfin/jellyfin.component.html | 4 ++-- .../ClientApp/src/app/wizard/emby/emby.component.ts | 1 - .../src/app/wizard/jellyfin/jellyfin.component.ts | 1 - 17 files changed, 24 insertions(+), 46 deletions(-) diff --git a/src/Ombi.Api.Emby/Models/PublicInfo.cs b/src/Ombi.Api.Emby/Models/PublicInfo.cs index 75be0f172..23db412b5 100644 --- a/src/Ombi.Api.Emby/Models/PublicInfo.cs +++ b/src/Ombi.Api.Emby/Models/PublicInfo.cs @@ -5,7 +5,6 @@ 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/Models/PublicInfo.cs b/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs index 56ae605e2..6687cf3c9 100644 --- a/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs +++ b/src/Ombi.Api.Jellyfin/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.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 index c900844a3..2f6a39f91 100644 --- a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.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.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, server?.ServerHostname, s.IsJellyfin); + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, server?.ServerHostname); } else { - obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, null, s.IsJellyfin); + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, null); } } 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 index 7a342160e..506341d7d 100644 --- a/src/Ombi.Helpers/JellyfinHelper.cs +++ b/src/Ombi.Helpers/JellyfinHelper.cs @@ -2,14 +2,10 @@ { public static class JellyfinHelper { - public static string GetJellyfinMediaUrl(string mediaId, string serverId, string customerServerUrl = null, bool isJellyfin = false) + public static string GetJellyfinMediaUrl(string mediaId, string serverId, string customerServerUrl = null) { //web/index.html#!/details|item - string path = "item"; - if (isJellyfin) - { - path = "details"; - } + string path = "details"; if (customerServerUrl.HasValue()) { if (!customerServerUrl.EndsWith("/")) @@ -20,7 +16,7 @@ } else { - return $"https://app.jellyfin.media/web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + return $"http://localhost:8096/web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; } } } 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..13821f5d9 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) { @@ -180,4 +180,4 @@ namespace Ombi.Schedule.Jobs.Emby 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 index 2453f2008..ff96e2130 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -58,7 +58,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin { await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Failed"); - _logger.LogError(e, "Exception when caching {1} for server {0}", server.Name, jellyfinSettings.IsJellyfin ? "Jellyfin" : "Jellyfin"); + _logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name); } } @@ -145,7 +145,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin Title = tvShow.Name, Type = JellyfinMediaType.Series, JellyfinId = tvShow.Id, - Url = JellyfinHelper.GetJellyfinMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname, settings.IsJellyfin), + Url = JellyfinHelper.GetJellyfinMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname), AddedAt = DateTime.UtcNow }); } @@ -228,4 +228,4 @@ namespace Ombi.Schedule.Jobs.Jellyfin } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs index 2c39616ff..c7322daa9 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs @@ -80,7 +80,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin Api = _apiFactory.CreateClient(settings); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) - .SendAsync(NotificationHub.NotificationEvent, $"{(settings.IsJellyfin ? "Jellyfin" : "Jellyfin")} User Importer Started"); + .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) { 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 index 0785137c5..3bee56848 100644 --- a/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs @@ -6,7 +6,6 @@ namespace Ombi.Core.Settings.Models.External public sealed class JellyfinSettings : Ombi.Settings.Settings.Models.Settings { public bool Enable { get; set; } - public bool IsJellyfin { get; set; } public List Servers { get; set; } = new List(); } diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts index bcd0e7d4a..744148ff9 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[]; } @@ -56,7 +55,6 @@ export interface IPublicInfo { export interface IJellyfinSettings extends ISettings { enable: boolean; - isJellyfin: boolean; servers: IJellyfinServer[]; } diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html index c1d8c1a83..e7dd34504 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html @@ -70,8 +70,8 @@ - Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1" - Current URL: "https://app.emby.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1 + Current URL: "{{server.serverHostname}}/#!/item/item.html?id=1" + Current URL: "https://app.emby.media/#!/item/item.html?id=1 diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html index 8eb1f90ed..4ff8991c1 100644 --- a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html @@ -70,8 +70,8 @@ - Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1" - Current URL: "https://app.jellyfin.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1 + Current URL: "{{server.serverHostname}}/#!/itemdetails.html?id=1" + Current URL: "https://app.jellyfin.media/#!/itemdetails.html?id=1 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..f528d8be3 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts @@ -20,7 +20,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.ts b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts index 4c42c88ac..e6b96d0f7 100644 --- a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts @@ -20,7 +20,6 @@ export class JellyfinComponent implements OnInit { public ngOnInit() { this.jellyfinSettings = { servers: [], - isJellyfin: false, id: 0, enable: true, }; From fce9e88b1e783c8f6e5a165376ef462207e01b87 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 00:26:47 -0500 Subject: [PATCH 10/21] Fix broken migration --- .../ExternalSqlite/20201212014227_Jellyfin.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs index 50d630b16..cd6d225d5 100644 --- a/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20201212014227_Jellyfin.cs @@ -14,11 +14,15 @@ namespace Ombi.Store.Migrations.ExternalSqlite { Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - AddedAt = table.Column(type: "TEXT", nullable: false), - JellyfinId = table.Column(type: "TEXT", nullable: false), - ProviderId = table.Column(type: "TEXT", nullable: true), Title = table.Column(type: "TEXT", nullable: true), - Type = table.Column(type: "INTEGER", nullable: false) + 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 => { @@ -32,13 +36,16 @@ namespace Ombi.Store.Migrations.ExternalSqlite { Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - AddedAt = table.Column(type: "TEXT", nullable: false), + 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), - SeasonNumber = table.Column(type: "INTEGER", nullable: false), - Title = 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 => { From 85e98e587f9b8b4315b156ab767dc49be6d8bb0e Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 02:55:57 -0500 Subject: [PATCH 11/21] Correct JellyfinUser to type 5 Make everything consistent here with the Store entity number. --- src/Ombi.Core/Models/UserDto.cs | 2 +- src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs | 3 +-- src/Ombi/ClientApp/src/app/interfaces/IUser.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Ombi.Core/Models/UserDto.cs b/src/Ombi.Core/Models/UserDto.cs index 6a8155bdb..7fbdb3465 100644 --- a/src/Ombi.Core/Models/UserDto.cs +++ b/src/Ombi.Core/Models/UserDto.cs @@ -20,6 +20,6 @@ namespace Ombi.Core.Models LocalUser = 1, PlexUser = 2, EmbyUser = 3, - JellyfinUser = 4 + JellyfinUser = 5 } } diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs index c7322daa9..d0d95ef3c 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs @@ -105,7 +105,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin if (!jellyfinUser.ConnectUserName.HasValue() && !jellyfinUser.Name.HasValue()) { - _log.LogInformation("Could not create Jellyfin user since the have no username, PlexUserId: {0}", jellyfinUser.Id); + _log.LogInformation("Could not create Jellyfin user since the have no username, JellyfinUserId: {0}", jellyfinUser.Id); continue; } var isConnectUser = jellyfinUser.ConnectUserName.HasValue(); @@ -114,7 +114,6 @@ namespace Ombi.Schedule.Jobs.Jellyfin { UserName = jellyfinUser.Name, ProviderUserId = jellyfinUser.Id, - Alias = isConnectUser ? jellyfinUser.Name : string.Empty, MovieRequestLimit = userManagementSettings.MovieRequestLimit, EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit }; diff --git a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts index 30f4837d2..b3ce9d165 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts @@ -48,7 +48,7 @@ export enum UserType { LocalUser = 1, PlexUser = 2, EmbyUser = 3, - JellyfinUser = 4, + JellyfinUser = 5, } export interface IIdentityResult { From 30cd96f82a1164e11eb4fe4efbb6456cb536601a Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 03:12:08 -0500 Subject: [PATCH 12/21] Fix bad controller replacement --- src/Ombi/Controllers/V1/JobController.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Ombi/Controllers/V1/JobController.cs b/src/Ombi/Controllers/V1/JobController.cs index 7f05474dc..b8ae1446b 100644 --- a/src/Ombi/Controllers/V1/JobController.cs +++ b/src/Ombi/Controllers/V1/JobController.cs @@ -93,10 +93,21 @@ namespace Ombi.Controllers.V1 } /// - /// Runs the Jellyfin User importer + /// Runs the Emby User importer /// /// [HttpPost("embyuserimporter")] + public async Task EmbyUserImporter() + { + await OmbiQuartz.TriggerJob(nameof(IEmbyUserImporter), "Emby"); + return true; + } + + /// + /// Runs the Jellyfin User importer + /// + /// + [HttpPost("jellyfinuserimporter")] public async Task JellyfinUserImporter() { await OmbiQuartz.TriggerJob(nameof(IJellyfinUserImporter), "Jellyfin"); From f52a74eba6b0073c94be4f8b7b16363baa7f0e73 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 16:04:16 -0500 Subject: [PATCH 13/21] Properly set usertype for Jellyfin users --- src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs index d0d95ef3c..790052309 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinUserImporter.cs @@ -103,20 +103,21 @@ namespace Ombi.Schedule.Jobs.Jellyfin if (existingJellyfinUser == null) { - if (!jellyfinUser.ConnectUserName.HasValue() && !jellyfinUser.Name.HasValue()) + if (!jellyfinUser.Name.HasValue()) { _log.LogInformation("Could not create Jellyfin user since the have no username, JellyfinUserId: {0}", jellyfinUser.Id); continue; } - var isConnectUser = jellyfinUser.ConnectUserName.HasValue(); // Create this users var newUser = new OmbiUser { UserName = jellyfinUser.Name, + UserType = UserType.JellyfinUser, ProviderUserId = jellyfinUser.Id, MovieRequestLimit = userManagementSettings.MovieRequestLimit, EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit }; + _log.LogInformation("Creating Jellyfin user {0}", newUser.UserName); var result = await _userManager.CreateAsync(newUser); if (!result.Succeeded) { From 71007ca5b7d1456acc1cfe992811a3b0ec3b77fe Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 16:18:03 -0500 Subject: [PATCH 14/21] Add MySQL migration --- .../20201212014227_Jellyfin.Designer.cs | 506 ++++++++++++++++++ .../ExternalMySql/20201212014227_Jellyfin.cs | 76 +++ .../ExternalMySqlContextModelSnapshot.cs | 113 ++++ 3 files changed, 695 insertions(+) create mode 100644 src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.Designer.cs create mode 100644 src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.cs diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.Designer.cs new file mode 100644 index 000000000..19189c2e0 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/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/ExternalMySql/20201212014227_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.cs new file mode 100644 index 000000000..f1eb6ebcf --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/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(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(nullable: true), + ProviderId = table.Column(nullable: true), + JellyfinId = table.Column(nullable: false), + Type = table.Column(nullable: false), + AddedAt = table.Column(nullable: false), + ImdbId = table.Column(nullable: true), + TheMovieDbId = table.Column(nullable: true), + TvDbId = table.Column(nullable: true), + Url = table.Column(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(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(nullable: true), + JellyfinId = table.Column(nullable: true), + EpisodeNumber = table.Column(nullable: false), + SeasonNumber = table.Column(nullable: false), + ParentId = table.Column(nullable: true), + ProviderId = table.Column(nullable: true), + AddedAt = table.Column(nullable: false), + TvDbId = table.Column(nullable: true), + ImdbId = table.Column(nullable: true), + TheMovieDbId = table.Column(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/ExternalMySql/ExternalMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs index 890e24b76..70312e542 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs @@ -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 CHARACTER SET utf8mb4"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("ProviderId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Title") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("TvDbId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("dateime(6)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("JellyfinId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ParentId") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("ProviderId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Title") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("TvDbId") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => { b.Property("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 } } From c03eaa12da9a75624c12389173bf89ee8aca2308 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 16:23:52 -0500 Subject: [PATCH 15/21] Remove superfluous newline --- src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs index d1a87ddbe..d2dcc0f8a 100644 --- a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs @@ -5,7 +5,6 @@ public string Primary { get; set; } public string Logo { get; set; } public string Thumb { get; set; } - public string Banner { get; set; } } -} \ No newline at end of file +} From 555406c1c49fe02bbffd68f098be251c8ba915d7 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 16:43:17 -0500 Subject: [PATCH 16/21] Fix up invalid MySQL migration Rename files, change import and namespace. --- ...in.Designer.cs => 20201212163118_Jellyfin.Designer.cs} | 6 +++--- ...01212014227_Jellyfin.cs => 20201212163118_Jellyfin.cs} | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) rename src/Ombi.Store/Migrations/ExternalMySql/{20201212014227_Jellyfin.Designer.cs => 20201212163118_Jellyfin.Designer.cs} (99%) rename src/Ombi.Store/Migrations/ExternalMySql/{20201212014227_Jellyfin.cs => 20201212163118_Jellyfin.cs} (95%) diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.Designer.cs similarity index 99% rename from src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.Designer.cs rename to src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.Designer.cs index 19189c2e0..93c128c5c 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.Designer.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Ombi.Store.Context.Sqlite; +using Ombi.Store.Context.MySql; -namespace Ombi.Store.Migrations.ExternalSqlite +namespace Ombi.Store.Migrations.ExternalMySql { - [DbContext(typeof(ExternalSqliteContext))] + [DbContext(typeof(ExternalMySqlContext))] [Migration("20201212014227_Jellyfin")] partial class Jellyfin { diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.cs similarity index 95% rename from src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.cs rename to src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.cs index f1eb6ebcf..bb23c95c9 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/20201212014227_Jellyfin.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.cs @@ -1,8 +1,8 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using System; -using System.Collections.Generic; +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; -namespace Ombi.Store.Migrations.ExternalSqlite +namespace Ombi.Store.Migrations.ExternalMySql { public partial class Jellyfin : Migration { From 6b24ab7f834b688d5662e496fe803ea0cdea8855 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 18:16:24 -0500 Subject: [PATCH 17/21] Correct tests for Jellyfin types --- .../Authentication/OmbiUserManagerTests.cs | 2 +- src/Ombi.Helpers.Tests/EmbyHelperTests.cs | 18 ------------ src/Ombi.Helpers.Tests/JellyfinHelperTests.cs | 29 +++++++++++++++++++ 3 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 src/Ombi.Helpers.Tests/JellyfinHelperTests.cs 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.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..e2c235443 --- /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 EmbyHelperTests + { + [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#!/item?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#!/item?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#!/item?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain_Https"); + } + } + } +} From bb60365109406ea2a4afd5532635c239c460fcb4 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 12 Dec 2020 18:28:40 -0500 Subject: [PATCH 18/21] Fix remaining tests --- .../Rule/Search/EmbyAvailabilityRuleTests.cs | 103 --------------- .../Search/JellyfinAvailabilityRuleTests.cs | 117 ++++++++++++++++++ src/Ombi.Helpers.Tests/JellyfinHelperTests.cs | 8 +- src/Ombi.Test.Common/MockHelper.cs | 2 +- 4 files changed, 122 insertions(+), 108 deletions(-) create mode 100644 src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs diff --git a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs index ed3e60631..132f51e49 100644 --- a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs @@ -115,107 +115,4 @@ namespace Ombi.Core.Tests.Rule.Search Assert.False(search.Available); } } - - 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#!/item?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 - { - 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/Rule/Search/JellyfinAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs new file mode 100644 index 000000000..5da69a181 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs @@ -0,0 +1,117 @@ +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 + { + 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.Helpers.Tests/JellyfinHelperTests.cs b/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs index e2c235443..df3f960d4 100644 --- a/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs +++ b/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs @@ -6,7 +6,7 @@ using System.Text; namespace Ombi.Helpers.Tests { [TestFixture] - public class EmbyHelperTests + public class JellyfinHelperTests { [TestCaseSource(nameof(UrlData))] public string TestUrl(string mediaId, string url, string serverId) @@ -20,9 +20,9 @@ namespace Ombi.Helpers.Tests get { var mediaId = 1; - yield return new TestCaseData(mediaId.ToString(), "http://google.com", "1").Returns($"http://google.com/web/index.html#!/item?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#!/item?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#!/item?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain_Https"); + 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.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()); From 6a062d5c40b017b754cf8a0aec3bcc4a198f532f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Sun, 3 Jan 2021 21:22:52 +0000 Subject: [PATCH 19/21] Fixed a bunch of jellyfin issues --- .../Engine/V2/MovieSearchEngineV2.cs | 1 + .../Rules/Search/JellyfinAvailabilityRule.cs | 3 +- src/Ombi.Helpers/StartupSingleton.cs | 3 + .../Jobs/Ombi/RefreshMetadata.cs | 4 +- ...cs => 20210103205509_Jellyfin.Designer.cs} | 201 +++++++++--------- ...Jellyfin.cs => 20210103205509_Jellyfin.cs} | 46 ++-- .../ExternalMySqlContextModelSnapshot.cs | 86 ++++---- src/Ombi.sln | 6 +- .../episode-request.component.html | 4 +- .../V1/External/TesterController.cs | 29 ++- src/Ombi/Ombi.csproj | 4 + src/Ombi/wwwroot/translations/en.json | 1 + 12 files changed, 214 insertions(+), 174 deletions(-) rename src/Ombi.Store/Migrations/ExternalMySql/{20201212163118_Jellyfin.Designer.cs => 20210103205509_Jellyfin.Designer.cs} (69%) rename src/Ombi.Store/Migrations/ExternalMySql/{20201212163118_Jellyfin.cs => 20210103205509_Jellyfin.cs} (52%) diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index b0a78fbb3..1e2110b38 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -287,6 +287,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/Rule/Rules/Search/JellyfinAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs index 2f6a39f91..0447458d9 100644 --- a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs @@ -74,7 +74,8 @@ namespace Ombi.Core.Rule.Rules.Search } else { - obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, null); + var firstServer = s.Servers?.FirstOrDefault(); + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, firstServer.ServerId, firstServer.FullUri); } } 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.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index 599cea66a..65c85b586 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -224,7 +224,7 @@ namespace Ombi.Schedule.Jobs.Ombi if (!hasImdb) { - var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId); + var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId, RequestType.TvShow); show.ImdbId = id; _jellyfinRepo.UpdateWithoutSave(show); } @@ -364,7 +364,7 @@ namespace Ombi.Schedule.Jobs.Ombi if (!movie.HasImdb) { - var imdbId = await GetImdbId(movie.HasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty); + var imdbId = await GetImdbId(movie.HasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty, RequestType.Movie); movie.ImdbId = imdbId; _jellyfinRepo.UpdateWithoutSave(movie); } diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.Designer.cs similarity index 69% rename from src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.Designer.cs rename to src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.Designer.cs index 93c128c5c..72f8d2376 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.Designer.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.Designer.cs @@ -9,23 +9,24 @@ using Ombi.Store.Context.MySql; namespace Ombi.Store.Migrations.ExternalMySql { [DbContext(typeof(ExternalMySqlContext))] - [Migration("20201212014227_Jellyfin")] + [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("INTEGER"); + .HasColumnType("int"); b.Property("TheMovieDbId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -36,35 +37,35 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("EmbyId") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("varchar(255)"); b.Property("ImdbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("ProviderId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TheMovieDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Type") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("Url") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -75,37 +76,37 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("EmbyId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("EpisodeNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ImdbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("ParentId") - .HasColumnType("TEXT"); + .HasColumnType("varchar(255)"); b.Property("ProviderId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("SeasonNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TheMovieDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -118,35 +119,35 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("ImdbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("JellyfinId") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("varchar(255)"); b.Property("ProviderId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TheMovieDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Type") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("Url") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -157,37 +158,37 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("EpisodeNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ImdbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("JellyfinId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("ParentId") - .HasColumnType("TEXT"); + .HasColumnType("varchar(255)"); b.Property("ProviderId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("SeasonNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TheMovieDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -200,31 +201,31 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("ArtistId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ForeignAlbumId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Monitored") - .HasColumnType("INTEGER"); + .HasColumnType("tinyint(1)"); b.Property("PercentOfTracks") - .HasColumnType("TEXT"); + .HasColumnType("decimal(65,30)"); b.Property("ReleaseDate") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TrackCount") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -235,19 +236,19 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ArtistId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ArtistName") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("ForeignArtistId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Monitored") - .HasColumnType("INTEGER"); + .HasColumnType("tinyint(1)"); b.HasKey("Id"); @@ -258,25 +259,25 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("EpisodeNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("GrandparentKey") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("Key") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ParentKey") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("SeasonNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -289,22 +290,22 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("ParentKey") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("PlexContentId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("PlexServerContentId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("SeasonKey") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("SeasonNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -317,40 +318,40 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("TEXT"); + .HasColumnType("datetime(6)"); b.Property("ImdbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Key") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("Quality") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("ReleaseYear") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("RequestId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TheMovieDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Title") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("TvDbId") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.Property("Type") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("Url") - .HasColumnType("TEXT"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -361,13 +362,13 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("HasFile") - .HasColumnType("INTEGER"); + .HasColumnType("tinyint(1)"); b.Property("TheMovieDbId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -378,10 +379,10 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TvDbId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -392,16 +393,16 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("EpisodeNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("SeasonNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TvDbId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -412,10 +413,10 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TvDbId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); @@ -426,19 +427,19 @@ namespace Ombi.Store.Migrations.ExternalMySql { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("EpisodeNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("HasFile") - .HasColumnType("INTEGER"); + .HasColumnType("tinyint(1)"); b.Property("SeasonNumber") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.Property("TvDbId") - .HasColumnType("INTEGER"); + .HasColumnType("int"); b.HasKey("Id"); diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.cs b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.cs similarity index 52% rename from src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.cs rename to src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.cs index bb23c95c9..cd7fe85d5 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/20201212163118_Jellyfin.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/20210103205509_Jellyfin.cs @@ -12,17 +12,17 @@ namespace Ombi.Store.Migrations.ExternalMySql name: "JellyfinContent", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "int", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - Title = table.Column(nullable: true), - ProviderId = table.Column(nullable: true), - JellyfinId = table.Column(nullable: false), - Type = table.Column(nullable: false), - AddedAt = table.Column(nullable: false), - ImdbId = table.Column(nullable: true), - TheMovieDbId = table.Column(nullable: true), - TvDbId = table.Column(nullable: true), - Url = table.Column(nullable: true) + 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 => { @@ -34,18 +34,18 @@ namespace Ombi.Store.Migrations.ExternalMySql name: "JellyfinEpisode", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "int", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - Title = table.Column(nullable: true), - JellyfinId = table.Column(nullable: true), - EpisodeNumber = table.Column(nullable: false), - SeasonNumber = table.Column(nullable: false), - ParentId = table.Column(nullable: true), - ProviderId = table.Column(nullable: true), - AddedAt = table.Column(nullable: false), - TvDbId = table.Column(nullable: true), - ImdbId = table.Column(nullable: true), - TheMovieDbId = table.Column(nullable: true) + 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 => { @@ -67,10 +67,10 @@ namespace Ombi.Store.Migrations.ExternalMySql protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "JellyfinContent"); + name: "JellyfinEpisode"); migrationBuilder.DropTable( - name: "JellyfinEpisode"); + name: "JellyfinContent"); } } } diff --git a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs index 70312e542..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"); @@ -123,29 +123,29 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("datetime(6)"); b.Property("ImdbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("JellyfinId") .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(255)"); 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"); @@ -159,34 +159,34 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("int"); b.Property("AddedAt") - .HasColumnType("dateime(6)"); + .HasColumnType("datetime(6)"); b.Property("EpisodeNumber") .HasColumnType("int"); b.Property("ImdbId") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("JellyfinId") - .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"); @@ -208,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)"); @@ -220,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"); @@ -240,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)"); @@ -275,7 +275,7 @@ namespace Ombi.Store.Migrations.ExternalMySql .HasColumnType("int"); b.Property("Title") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.HasKey("Id"); @@ -322,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"); diff --git a/src/Ombi.sln b/src/Ombi.sln index 29decbc1c..658d37c3b 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -39,7 +39,7 @@ 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", "{08FF107D-31E1-470D-AF86-E09B015CEE06}" +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 @@ -171,6 +171,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 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..4802ccaaa 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 @@ -24,9 +24,9 @@ Season {{season.seasonNumber}} Season {{season.seasonNumber}} - +
diff --git a/src/Ombi/Controllers/V1/External/TesterController.cs b/src/Ombi/Controllers/V1/External/TesterController.cs index 0dbaac688..0c80b051c 100644 --- a/src/Ombi/Controllers/V1/External/TesterController.cs +++ b/src/Ombi/Controllers/V1/External/TesterController.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Ombi.Api.CouchPotato; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.Lidarr; using Ombi.Api.Plex; using Ombi.Api.Radarr; @@ -37,6 +38,7 @@ namespace Ombi.Controllers.V1.External [Produces("application/json")] public class TesterController : Controller { + /// /// Initializes a new instance of the class. /// @@ -44,7 +46,8 @@ namespace Ombi.Controllers.V1.External IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPlexApi plex, IEmbyApiFactory emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger log, IEmailProvider provider, ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, ILegacyMobileNotification mobileNotification, - ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um, IWebhookNotification webhookNotification) + ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um, IWebhookNotification webhookNotification, + IJellyfinApi jellyfinApi) { Service = service; DiscordNotification = notification; @@ -69,6 +72,7 @@ namespace Ombi.Controllers.V1.External WhatsAppApi = whatsAppApi; UserManager = um; WebhookNotification = webhookNotification; + _jellyfinApi = jellyfinApi; } private INotificationService Service { get; } @@ -93,7 +97,8 @@ namespace Ombi.Controllers.V1.External private ILegacyMobileNotification MobileNotification { get; } private ILidarrApi LidarrApi { get; } private IWhatsAppApi WhatsAppApi { get; } - private OmbiUserManager UserManager {get;} + private OmbiUserManager UserManager {get; } + private readonly IJellyfinApi _jellyfinApi; /// /// Sends a test message to discord using the provided settings @@ -333,6 +338,26 @@ namespace Ombi.Controllers.V1.External } } + /// + /// Checks if we can connect to Jellyfin with the provided settings + /// + /// + /// + [HttpPost("jellyfin")] + public async Task Jellyfin([FromBody] JellyfinServers settings) + { + try + { + var result = await _jellyfinApi.GetUsers(settings.FullUri, settings.ApiKey); + return result.Any(); + } + catch (Exception e) + { + Log.LogError(LoggingEvents.Api, e, "Could not test Jellyfin"); + return false; + } + } + /// /// Checks if we can connect to Radarr with the provided settings /// diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 2c615f57c..0b86b06f1 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -53,6 +53,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index 665811489..4108bb848 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -92,6 +92,7 @@ "TheatricalRelease": "Theatrical Release: {{date}}", "ViewOnPlex": "View On Plex", "ViewOnEmby": "View On Emby", + "ViewOnJellyfin": "View On Jellyfin", "RequestAdded": "Request for {{title}} has been added successfully", "Similar": "Similar", "Refine": "Refine", From b888a51b5b9b1b22a2887c0572aff4f48bb3735a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Sun, 3 Jan 2021 21:43:53 +0000 Subject: [PATCH 20/21] last tweak --- .../src/app/settings/jellyfin/jellyfin.component.html | 4 ++-- .../ClientApp/src/app/settings/jellyfin/jellyfin.component.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html index 4ff8991c1..66b4b39f6 100644 --- a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html @@ -58,7 +58,7 @@
-
+
Base URL @@ -71,7 +71,7 @@ Current URL: "{{server.serverHostname}}/#!/itemdetails.html?id=1" - Current URL: "https://app.jellyfin.media/#!/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.ts b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts index 2ae0d331d..133ae4b39 100644 --- a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts @@ -15,6 +15,7 @@ export class JellyfinComponent implements OnInit { public hasDiscoveredOrDirty: boolean; selected = new FormControl(0); + constructor(private settingsService: SettingsService, private notificationService: NotificationService, private testerService: TesterService, From 2e00aa00f087861c1e7416bb8e9e6a4ad2ebb85c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 4 Jan 2021 09:00:41 +0000 Subject: [PATCH 21/21] fixed unit test --- .../Rule/Search/JellyfinAvailabilityRuleTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs index 5da69a181..1b838f102 100644 --- a/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs @@ -84,6 +84,8 @@ namespace Ombi.Core.Tests.Rule.Search { new JellyfinServers { + Ip = "8080", + Port = 9090, ServerHostname = string.Empty, ServerId = "8" }