diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb51423c..fb91d9d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog +## (unreleased) + +### **New Features** + +- Updated the Emby availability checker to bring it more in line with what we do with Plex. [TidusJar] + +- Added the ability to impersonate a user when using the API Key. This allows people to use the API and request as a certain user. #2363. [Jamie Rees] + +- Added more background images and it will loop through the available ones. [Jamie Rees] + +- Added chunk hashing to resolve #2330. [Jamie Rees] + +- Added API at /api/v1/status/info to get branch and version information #2331. [Jamie Rees] + +- Update to .net 2.1.1. [Jamie] + +### **Fixes** + +- Fix #2322 caused by continue statement inside try catch block. [Anojh] + +- Fixed #2367. [TidusJar] + +- Fixed the issue where you could not delete a user #2365. [TidusJar] + +- Another attempt to fix #2366. [Jamie Rees] + +- Fixed the Plex OAuth warning. [Jamie] + +- Revert "Fixed Plex OAuth, should no longer show Insecure warning" [Jamie Rees] + +- Fixed Plex OAuth, should no longer show Insecure warning. [Jamie Rees] + +- Fixed the View On Emby URL since the Link changed #2368. [Jamie Rees] + +- Fixed the issue where episodes were not being marked as available in the search #2367. [Jamie Rees] + +- Fixed #2371. [Jamie Rees] + +- Fixed collection issues in Emby #2366. [Jamie Rees] + +- Do not delete the Emby Information every time we run, let's keep the content now. [Jamie Rees] + +- Emby Improvements: Batch up the amount we get from the server. [Jamie Rees] + +- Log errors when they are uncaught. [Jamie Rees] + +- Fix unclosed table tags causing overflow #2322. [Anojh] + +- This should now fix #2350. [Jamie] + +- Improve the validation around the Application URL. [Jamie Rees] + +- Fixed #2341. [Jamie Rees] + +- Stop spamming errors when FanArt doesn't have the image. [Jamie Rees] + +- Fixed #2338. [Jamie Rees] + +- Removed some logging statements. [Jamie Rees] + +- Fixed the api key being case sensative #2350. [Jamie Rees] + +- Improved the Emby API #2230 Thanks Luke! [Jamie Rees] + +- Revert. [Jamie Rees] + +- Fixed a small error in the Mobile Notification Provider. [Jamie Rees] + +- Minor style tweaks. [Randall Bruder] + +- Downgrade to .net core 2.0. [Jamie Rees] + +- Downgrade Microsoft.AspNetCore.All package back to 2.0.8. [Jamie Rees] + +- Removed old code. [Jamie Rees] + +- Swap out the old way of validating the API key with a real middlewear this time. [Jamie Rees] + + ## v3.0.3421 (2018-06-23) ### **New Features** diff --git a/appveyor.yml b/appveyor.yml index 39d67b91d..862993a21 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,19 +15,19 @@ test: off after_build: - cmd: >- - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\windows.zip" + appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\windows.zip" - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\osx.tar.gz" + appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\osx.tar.gz" - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux.tar.gz" + appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\linux.tar.gz" - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm.tar.gz" + appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\linux-arm.tar.gz" - appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\windows-32bit.zip" + appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\windows-32bit.zip" # appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm64.tar.gz" diff --git a/build.cake b/build.cake index 6e600407f..8dcdbe58f 100644 --- a/build.cake +++ b/build.cake @@ -26,7 +26,7 @@ var csProj = "./src/Ombi/Ombi.csproj"; // Path to the project.csproj var solutionFile = "Ombi.sln"; // Solution file if needed GitVersion versionInfo = null; -var frameworkVer = "netcoreapp2.0"; +var frameworkVer = "netcoreapp2.1"; var buildSettings = new DotNetCoreBuildSettings { diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index 3ac70c844..fcb989094 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -91,27 +91,31 @@ namespace Ombi.Api.Emby request.AddContentHeader("Content-Type", "application/json"); } - 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($"emby/users/{userId}/items?parentId={mediaId}", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); - return await Api.Request>(request); + request.AddQueryString("Fields", "ProviderIds,Overview"); + + request.AddQueryString("VirtualItem", "False"); + + return await Api.Request>(request); } - public async Task> GetAllMovies(string apiKey, 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); + return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); } - public async Task> GetAllEpisodes(string apiKey, 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); + return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); } - public async Task> GetAllShows(string apiKey, 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); + return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); } public async Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) @@ -145,7 +149,25 @@ namespace Ombi.Api.Emby request.AddQueryString("IncludeItemTypes", type); request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); - request.AddQueryString("VirtualItem","False"); + request.AddQueryString("VirtualItem", "False"); + + AddHeaders(request, apiKey); + + + 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) + { + var request = new Request($"emby/users/{userId}/items", baseUri, HttpMethod.Get); + + request.AddQueryString("Recursive", true.ToString()); + request.AddQueryString("IncludeItemTypes", type); + request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); + request.AddQueryString("startIndex", startIndex.ToString()); + request.AddQueryString("limit", count.ToString()); + + request.AddQueryString("VirtualItem", "False"); AddHeaders(request, apiKey); diff --git a/src/Ombi.Api.Emby/IEmbyApi.cs b/src/Ombi.Api.Emby/IEmbyApi.cs index 625ae3c13..b4641ea5f 100644 --- a/src/Ombi.Api.Emby/IEmbyApi.cs +++ b/src/Ombi.Api.Emby/IEmbyApi.cs @@ -14,12 +14,17 @@ namespace Ombi.Api.Emby Task LogIn(string username, string password, string apiKey, string baseUri); Task LoginConnectUser(string username, string password); - Task> GetAllMovies(string apiKey, string userId, string baseUri); - Task> GetAllEpisodes(string apiKey, string userId, string baseUri); - Task> GetAllShows(string apiKey, string userId, string baseUri); + Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, + string baseUri); - Task> GetCollection(string mediaId, string apiKey, string userId, - string baseUrl); + 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); diff --git a/src/Ombi.Api.Plex/IPlexApi.cs b/src/Ombi.Api.Plex/IPlexApi.cs index cc61dfa5d..2dd1a638f 100644 --- a/src/Ombi.Api.Plex/IPlexApi.cs +++ b/src/Ombi.Api.Plex/IPlexApi.cs @@ -22,8 +22,7 @@ namespace Ombi.Api.Plex Task GetUsers(string authToken); Task GetAccount(string authToken); Task GetRecentlyAdded(string authToken, string uri, string sectionId); - Task CreatePin(); Task GetPin(int pinId); - Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard); + Task GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard); } } \ No newline at end of file diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs index a16dee9ec..4276f6203 100644 --- a/src/Ombi.Api.Plex/PlexApi.cs +++ b/src/Ombi.Api.Plex/PlexApi.cs @@ -16,14 +16,16 @@ namespace Ombi.Api.Plex { public class PlexApi : IPlexApi { - public PlexApi(IApi api, ISettingsService settings) + public PlexApi(IApi api, ISettingsService settings, ISettingsService p) { Api = api; _custom = settings; + _plexSettings = p; } private IApi Api { get; } private readonly ISettingsService _custom; + private readonly ISettingsService _plexSettings; private string _app; private string ApplicationName @@ -69,7 +71,7 @@ namespace Ombi.Api.Plex }; var request = new Request(SignInUri, string.Empty, HttpMethod.Post); - AddHeaders(request); + await AddHeaders(request); request.AddJsonBody(userModel); var obj = await Api.Request(request); @@ -80,14 +82,14 @@ namespace Ombi.Api.Plex public async Task GetStatus(string authToken, string uri) { var request = new Request(uri, string.Empty, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } public async Task GetAccount(string authToken) { var request = new Request(GetAccountUri, string.Empty, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } @@ -95,7 +97,7 @@ namespace Ombi.Api.Plex { var request = new Request(ServerUri, string.Empty, HttpMethod.Get, ContentType.Xml); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } @@ -103,14 +105,14 @@ namespace Ombi.Api.Plex public async Task GetLibrarySections(string authToken, string plexFullHost) { var request = new Request("library/sections", plexFullHost, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } public async Task GetLibrary(string authToken, string plexFullHost, string libraryId) { var request = new Request($"library/sections/{libraryId}/all", plexFullHost, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } @@ -128,21 +130,21 @@ namespace Ombi.Api.Plex public async Task GetEpisodeMetaData(string authToken, string plexFullHost, int ratingKey) { var request = new Request($"/library/metadata/{ratingKey}", plexFullHost, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } public async Task GetMetadata(string authToken, string plexFullHost, int itemId) { var request = new Request($"library/metadata/{itemId}", plexFullHost, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } public async Task GetSeasons(string authToken, string plexFullHost, int ratingKey) { var request = new Request($"library/metadata/{ratingKey}/children", plexFullHost, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); return await Api.Request(request); } @@ -161,9 +163,9 @@ namespace Ombi.Api.Plex request.AddQueryString("type", "4"); AddLimitHeaders(request, start, retCount); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); - return await Api.Request(request); + return await Api.Request(request); } /// @@ -174,8 +176,8 @@ namespace Ombi.Api.Plex /// public async Task GetUsers(string authToken) { - var request = new Request(string.Empty,FriendsUri, HttpMethod.Get, ContentType.Xml); - AddHeaders(request, authToken); + var request = new Request(string.Empty, FriendsUri, HttpMethod.Get, ContentType.Xml); + await AddHeaders(request, authToken); return await Api.Request(request); } @@ -183,43 +185,40 @@ namespace Ombi.Api.Plex public async Task GetRecentlyAdded(string authToken, string uri, string sectionId) { var request = new Request($"library/sections/{sectionId}/recentlyAdded", uri, HttpMethod.Get); - AddHeaders(request, authToken); + await AddHeaders(request, authToken); AddLimitHeaders(request, 0, 50); return await Api.Request(request); } - public async Task CreatePin() - { - var request = new Request($"api/v2/pins", "https://plex.tv/", HttpMethod.Post); - request.AddQueryString("strong", "true"); - AddHeaders(request); - - return await Api.Request(request); - } - public async Task GetPin(int pinId) { var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get); - AddHeaders(request); + await AddHeaders(request); return await Api.Request(request); } - public Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard) + public async Task GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard) { var request = new Request("auth#", "https://app.plex.tv", HttpMethod.Get); - AddHeaders(request); - var forwardUrl = wizard - ? new Request($"Wizard/OAuth/{pinId}", applicationUrl, HttpMethod.Get) + await AddHeaders(request); + var forwardUrl = wizard + ? new Request($"Wizard/OAuth/{pinId}", applicationUrl, HttpMethod.Get) : new Request($"Login/OAuth/{pinId}", applicationUrl, HttpMethod.Get); request.AddQueryString("forwardUrl", forwardUrl.FullUri.ToString()); request.AddQueryString("pinID", pinId.ToString()); request.AddQueryString("code", code); - request.AddQueryString("context[device][product]", "Ombi"); + request.AddQueryString("context[device][product]", ApplicationName); request.AddQueryString("context[device][environment]", "bundled"); - request.AddQueryString("clientID", $"OmbiV3"); + request.AddQueryString("context[device][layout]", "desktop"); + request.AddQueryString("context[device][platform]", "Web"); + request.AddQueryString("context[device][device]", "Ombi (Web)"); + + var s = await GetSettings(); + await CheckInstallId(s); + request.AddQueryString("clientID", s.InstallId.ToString("N")); if (request.FullUri.Fragment.Equals("#")) { @@ -238,21 +237,25 @@ namespace Ombi.Api.Plex /// /// /// - private void AddHeaders(Request request, string authToken) + private async Task AddHeaders(Request request, string authToken) { request.AddHeader("X-Plex-Token", authToken); - AddHeaders(request); + await AddHeaders(request); } /// /// Adds the main required headers to the Plex Request /// /// - private void AddHeaders(Request request) + private async Task AddHeaders(Request request) { - request.AddHeader("X-Plex-Client-Identifier", $"OmbiV3"); + var s = await GetSettings(); + await CheckInstallId(s); + request.AddHeader("X-Plex-Client-Identifier", s.InstallId.ToString("N")); request.AddHeader("X-Plex-Product", ApplicationName); request.AddHeader("X-Plex-Version", "3"); + request.AddHeader("X-Plex-Device", "Ombi (Web)"); + request.AddHeader("X-Plex-Platform", "Web"); request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml"); request.AddHeader("Accept", "application/json"); } @@ -262,5 +265,19 @@ namespace Ombi.Api.Plex request.AddHeader("X-Plex-Container-Start", from.ToString()); request.AddHeader("X-Plex-Container-Size", to.ToString()); } + private async Task CheckInstallId(PlexSettings s) + { + if (s.InstallId == null || s.InstallId == Guid.Empty) + { + s.InstallId = Guid.NewGuid(); + await _plexSettings.SaveSettingsAsync(s); + } + } + + private PlexSettings _settings; + private async Task GetSettings() + { + return _settings ?? (_settings = await _plexSettings.GetSettingsAsync()); + } } } diff --git a/src/Ombi.Api.Radarr/Ombi.Api.Radarr.csproj b/src/Ombi.Api.Radarr/Ombi.Api.Radarr.csproj index d2b605337..0c615f301 100644 --- a/src/Ombi.Api.Radarr/Ombi.Api.Radarr.csproj +++ b/src/Ombi.Api.Radarr/Ombi.Api.Radarr.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Ombi.Api.Service/Ombi.Api.Service.csproj b/src/Ombi.Api.Service/Ombi.Api.Service.csproj index 67f37c80d..8cbddd874 100644 --- a/src/Ombi.Api.Service/Ombi.Api.Service.csproj +++ b/src/Ombi.Api.Service/Ombi.Api.Service.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Ombi.Api/Ombi.Api.csproj b/src/Ombi.Api/Ombi.Api.csproj index 32fc60bb6..a37c128fb 100644 --- a/src/Ombi.Api/Ombi.Api.csproj +++ b/src/Ombi.Api/Ombi.Api.csproj @@ -9,9 +9,9 @@ - + - + diff --git a/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj b/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj index db98876df..8f0abee8f 100644 --- a/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj +++ b/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp2.1 @@ -9,7 +9,7 @@ - + diff --git a/src/Ombi.Core/Authentication/PlexOAuthManager.cs b/src/Ombi.Core/Authentication/PlexOAuthManager.cs index 37ed7d2f7..803176d74 100644 --- a/src/Ombi.Core/Authentication/PlexOAuthManager.cs +++ b/src/Ombi.Core/Authentication/PlexOAuthManager.cs @@ -20,12 +20,6 @@ namespace Ombi.Core.Authentication private readonly IPlexApi _api; private readonly ISettingsService _customizationSettingsService; - public async Task RequestPin() - { - var pin = await _api.CreatePin(); - return pin; - } - public async Task GetAccessTokenFromPin(int pinId) { var pin = await _api.GetPin(pinId); @@ -58,14 +52,14 @@ namespace Ombi.Core.Authentication public async Task GetOAuthUrl(int pinId, string code, string websiteAddress = null) { var settings = await _customizationSettingsService.GetSettingsAsync(); - var url = _api.GetOAuthUrl(pinId, code, settings.ApplicationUrl.IsNullOrEmpty() ? websiteAddress : settings.ApplicationUrl, false); + var url = await _api.GetOAuthUrl(pinId, code, settings.ApplicationUrl.IsNullOrEmpty() ? websiteAddress : settings.ApplicationUrl, false); return url; } - public Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress) + public async Task GetWizardOAuthUrl(int pinId, string code, string websiteAddress) { - var url = _api.GetOAuthUrl(pinId, code, websiteAddress, true); + var url = await _api.GetOAuthUrl(pinId, code, websiteAddress, true); return url; } } diff --git a/src/Ombi.Core/IPlexOAuthManager.cs b/src/Ombi.Core/IPlexOAuthManager.cs index 9c4f0582e..a5c0c44ff 100644 --- a/src/Ombi.Core/IPlexOAuthManager.cs +++ b/src/Ombi.Core/IPlexOAuthManager.cs @@ -1,16 +1,14 @@ using System; using System.Threading.Tasks; using Ombi.Api.Plex.Models; -using Ombi.Api.Plex.Models.OAuth; namespace Ombi.Core.Authentication { public interface IPlexOAuthManager { Task GetAccessTokenFromPin(int pinId); - Task RequestPin(); Task GetOAuthUrl(int pinId, string code, string websiteAddress = null); - Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress); + Task GetWizardOAuthUrl(int pinId, string code, string websiteAddress); Task GetAccount(string accessToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Ombi.Core.csproj b/src/Ombi.Core/Ombi.Core.csproj index 2037113bd..104db24fc 100644 --- a/src/Ombi.Core/Ombi.Core.csproj +++ b/src/Ombi.Core/Ombi.Core.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs index 4f2f56b28..486de9ea8 100644 --- a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Core.Models.Search; @@ -59,10 +61,19 @@ namespace Ombi.Core.Rule.Rules.Search { EmbyEpisode epExists = null; - epExists = await allEpisodes.FirstOrDefaultAsync(x => - x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && - x.Series.ProviderId == item.ProviderId.ToString()); - + if (item.HasImdb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber + && e.ImdbId == item.ImdbId); + } if (item.HasTvDb && epExists == null) + { + epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber + && e.Series.TvDbId == item.TvDbId); + } if (item.HasTheMovieDb && epExists == null) + { + epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber + && e.TheMovieDbId == item.TheMovieDbId); + } if (epExists != null) { diff --git a/src/Ombi.Core/Senders/TvSender.cs b/src/Ombi.Core/Senders/TvSender.cs index 12124d270..2cccd6778 100644 --- a/src/Ombi.Core/Senders/TvSender.cs +++ b/src/Ombi.Core/Senders/TvSender.cs @@ -255,7 +255,12 @@ namespace Ombi.Core.Senders { // We have the same amount of requests as all of the episodes in the season. var existingSeason = - result.seasons.First(x => x.seasonNumber == season.SeasonNumber); + result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); + if (existingSeason == null) + { + Logger.LogError("The sonarr ep count was the same as out request count, but could not match the season number {0}", season.SeasonNumber); + continue; + } existingSeason.monitored = true; seriesChanges = true; } diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index 675f6461b..2e7f984a7 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/src/Ombi.Helpers/EmbyHelper.cs b/src/Ombi.Helpers/EmbyHelper.cs index cf6904a0c..567bcfe7e 100644 --- a/src/Ombi.Helpers/EmbyHelper.cs +++ b/src/Ombi.Helpers/EmbyHelper.cs @@ -10,7 +10,7 @@ namespace Ombi.Helpers public static string GetEmbyMediaUrl(string mediaId) { var url = - $"http://app.emby.media/itemdetails.html?id={mediaId}"; + $"http://app.emby.media/#!/itemdetails.html?id={mediaId}"; return url; } } diff --git a/src/Ombi.Helpers/Ombi.Helpers.csproj b/src/Ombi.Helpers/Ombi.Helpers.csproj index 12c6fecc4..e94afc816 100644 --- a/src/Ombi.Helpers/Ombi.Helpers.csproj +++ b/src/Ombi.Helpers/Ombi.Helpers.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj b/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj index 339986ef5..de1c55533 100644 --- a/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj +++ b/src/Ombi.Notifications.Tests/Ombi.Notifications.Tests.csproj @@ -1,14 +1,14 @@  - netcoreapp2.0 + netcoreapp2.1 - + diff --git a/src/Ombi.Notifications/Ombi.Notifications.csproj b/src/Ombi.Notifications/Ombi.Notifications.csproj index 46a64072e..2b5c95154 100644 --- a/src/Ombi.Notifications/Ombi.Notifications.csproj +++ b/src/Ombi.Notifications/Ombi.Notifications.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj index bdfbd60e6..a124f01bd 100644 --- a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj +++ b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj @@ -1,16 +1,16 @@ - netcoreapp2.0 + netcoreapp2.1 - + - + diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyAvaliabilityChecker.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyAvaliabilityChecker.cs index 59b5adc96..ec675ebd8 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyAvaliabilityChecker.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyAvaliabilityChecker.cs @@ -121,24 +121,53 @@ namespace Ombi.Schedule.Jobs.Emby foreach (var child in tv) { - IQueryable seriesEpisodes; - if (child.ParentRequest.TvDbId > 0) + + var useImdb = false; + var useTvDb = false; + if (child.ParentRequest.ImdbId.HasValue()) + { + useImdb = true; + } + + if (child.ParentRequest.TvDbId.ToString().HasValue()) { - seriesEpisodes = embyEpisodes.Where(x => x.Series.TvDbId == child.ParentRequest.TvDbId.ToString()); + useTvDb = true; } - else if(child.ParentRequest.ImdbId.HasValue()) + + var tvDbId = child.ParentRequest.TvDbId; + var imdbId = child.ParentRequest.ImdbId; + IQueryable seriesEpisodes = null; + if (useImdb) { - seriesEpisodes = embyEpisodes.Where(x => x.Series.ImdbId == child.ParentRequest.ImdbId); + seriesEpisodes = embyEpisodes.Where(x => x.Series.ImdbId == imdbId.ToString()); } - else + + if (useTvDb && (seriesEpisodes == null || !seriesEpisodes.Any())) + { + seriesEpisodes = embyEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString()); + } + + if (seriesEpisodes == null) { continue; } + if (!seriesEpisodes.Any()) + { + // Let's try and match the series by name + seriesEpisodes = embyEpisodes.Where(x => + x.Series.Title.Equals(child.Title, StringComparison.CurrentCultureIgnoreCase)); + } + 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); @@ -160,9 +189,9 @@ namespace Ombi.Schedule.Jobs.Emby { DateTime = DateTime.Now, NotificationType = NotificationType.RequestAvailable, - RequestId = child.ParentRequestId, + RequestId = child.Id, RequestType = RequestType.TvShow, - Recipient = child.RequestedUser.Email, + Recipient = child.RequestedUser.Email })); } } diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index af8125ab2..7002eec41 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -68,40 +68,96 @@ namespace Ombi.Schedule.Jobs.Emby if (!ValidateSettings(server)) return; - await _repo.ExecuteSql("DELETE FROM EmbyEpisode"); - await _repo.ExecuteSql("DELETE FROM EmbyContent"); + //await _repo.ExecuteSql("DELETE FROM EmbyEpisode"); + //await _repo.ExecuteSql("DELETE FROM EmbyContent"); + + var movies = await _api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var totalCount = movies.TotalRecordCount; + var processed = 1; - var movies = await _api.GetAllMovies(server.ApiKey, server.AdministratorId, server.FullUri); var mediaToAdd = new HashSet(); - foreach (var movie in movies.Items) + + while (processed < totalCount) { - // Regular movie - await ProcessMovies(movie, mediaToAdd); + 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); + } + + processed++; + } + else + { + processed++; + // Regular movie + await ProcessMovies(movie, mediaToAdd); + } + } + + // 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, server.AdministratorId, server.FullUri); - foreach (var tvShow in tv.Items) - { - if (string.IsNullOrEmpty(tvShow.ProviderIds?.Tvdb)) + + // 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) { - Log.Error("Provider Id on tv {0} is null", tvShow.Name); - continue; - } + try + { - var existingTv = await _repo.GetByEmbyId(tvShow.Id); - if (existingTv == null) - mediaToAdd.Add(new EmbyContent + processed++; + if (string.IsNullOrEmpty(tvShow.ProviderIds?.Tvdb)) + { + _logger.LogInformation("Provider Id on tv {0} is null", tvShow.Name); + continue; + } + + var existingTv = await _repo.GetByEmbyId(tvShow.Id); + if (existingTv == null) + { + _logger.LogDebug("Adding new TV Show {0}", tvShow.Name); + mediaToAdd.Add(new EmbyContent + { + TvDbId = tvShow.ProviderIds?.Tvdb, + ImdbId = tvShow.ProviderIds?.Imdb, + TheMovieDbId = tvShow.ProviderIds?.Tmdb, + Title = tvShow.Name, + Type = EmbyMediaType.Series, + EmbyId = tvShow.Id, + Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id), + AddedAt = DateTime.UtcNow + }); + } + else + { + _logger.LogDebug("We already have TV Show {0}", tvShow.Name); + } + + } + catch (Exception) { - TvDbId = tvShow.ProviderIds?.Tvdb, - ImdbId = tvShow.ProviderIds?.Imdb, - TheMovieDbId = tvShow.ProviderIds?.Tmdb, - Title = tvShow.Name, - Type = EmbyMediaType.Series, - EmbyId = tvShow.Id, - Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id), - AddedAt = DateTime.UtcNow - }); + + 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()) @@ -112,8 +168,10 @@ namespace Ombi.Schedule.Jobs.Emby { // Check if it exists var existingMovie = await _repo.GetByEmbyId(movieInfo.Id); - - if (existingMovie == null) + var alreadyGoingToAdd = content.Any(x => x.EmbyId == movieInfo.Id); + if (existingMovie == null && !alreadyGoingToAdd) + { + _logger.LogDebug("Adding new movie {0}", movieInfo.Name); content.Add(new EmbyContent { ImdbId = movieInfo.ProviderIds.Imdb, @@ -124,6 +182,12 @@ namespace Ombi.Schedule.Jobs.Emby Url = EmbyHelper.GetEmbyMediaUrl(movieInfo.Id), AddedAt = DateTime.UtcNow, }); + } + else + { + // we have this + _logger.LogDebug("We already have movie {0}", movieInfo.Name); + } } private bool ValidateSettings(EmbyServers server) diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs index b5ed6d443..f66ff89ab 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs @@ -73,37 +73,51 @@ namespace Ombi.Schedule.Jobs.Emby private async Task CacheEpisodes(EmbyServers server) { - var allEpisodes = await _api.GetAllEpisodes(server.ApiKey, server.AdministratorId, server.FullUri); - var epToAdd = new List(); - - foreach (var ep in allEpisodes.Items) + 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) { - // Let's make sure we have the parent request, stop those pesky forign key errors, - // Damn me having data integrity - var parent = await _repo.GetByEmbyId(ep.SeriesId); - if (parent == null) + foreach (var ep in allEpisodes.Items) { - _logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this", ep.Name); - continue; - } + processed++; + // Let's make sure we have the parent request, stop those pesky forign key errors, + // Damn me having data integrity + var parent = await _repo.GetByEmbyId(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.GetEpisodeByEmbyId(ep.Id); - if (existingEpisode == null) - { - // add it - epToAdd.Add(new EmbyEpisode + var existingEpisode = await _repo.GetEpisodeByEmbyId(ep.Id); + // Make sure it's not in the hashset too + var existingInList = epToAdd.Any(x => x.EmbyId == ep.Id); + + if (existingEpisode == null && !existingInList) { - EmbyId = 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 - }); + _logger.LogDebug("Adding new episode {0} to parent {1}", ep.Name, ep.SeriesName); + // add it + epToAdd.Add(new EmbyEpisode + { + EmbyId = 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 + }); + } } + + await _repo.AddRange(epToAdd); + epToAdd.Clear(); + allEpisodes = await _api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); } if (epToAdd.Any()) diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index 8f88a4a18..6e89d167e 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -492,41 +492,42 @@ namespace Ombi.Schedule.Jobs.Ombi var orderedTv = series.OrderByDescending(x => x.AddedAt); foreach (var t in orderedTv) { - try + if (!t.HasTvDb) { - if (!t.HasTvDb) + // We may need to use themoviedb for the imdbid or their own id to get info + if (t.HasTheMovieDb) { - // We may need to use themoviedb for the imdbid or their own id to get info - if (t.HasTheMovieDb) + int.TryParse(t.TheMovieDbId, out var movieId); + var externals = await _movieApi.GetTvExternals(movieId); + if (externals == null || externals.tvdb_id <= 0) { - int.TryParse(t.TheMovieDbId, out var movieId); - var externals = await _movieApi.GetTvExternals(movieId); - if (externals == null || externals.tvdb_id <= 0) - { - continue; - } - t.TvDbId = externals.tvdb_id.ToString(); + continue; } - // WE could check the below but we need to get the moviedb and then perform the above, let the metadata job figure this out. - //else if(t.HasImdb) - //{ - // // Check the imdbid - // var externals = await _movieApi.Find(t.ImdbId, ExternalSource.imdb_id); - // if (externals?.tv_results == null || externals.tv_results.Length <= 0) - // { - // continue; - // } - // t.TvDbId = externals.tv_results.FirstOrDefault()..ToString(); - //} - + t.TvDbId = externals.tvdb_id.ToString(); } + // WE could check the below but we need to get the moviedb and then perform the above, let the metadata job figure this out. + //else if(t.HasImdb) + //{ + // // Check the imdbid + // var externals = await _movieApi.Find(t.ImdbId, ExternalSource.imdb_id); + // if (externals?.tv_results == null || externals.tv_results.Length <= 0) + // { + // continue; + // } + // t.TvDbId = externals.tv_results.FirstOrDefault()..ToString(); + //} - int.TryParse(t.TvDbId, out var tvdbId); - var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId); - if (info == null) - { - 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)) { @@ -588,7 +589,7 @@ namespace Ombi.Schedule.Jobs.Ombi { AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); } - + } catch (Exception e) { @@ -676,20 +677,20 @@ namespace Ombi.Schedule.Jobs.Ombi var orderedTv = series.OrderByDescending(x => x.AddedAt); foreach (var t in orderedTv) { - try + if (!t.TvDbId.HasValue()) { - if (!t.TvDbId.HasValue()) - { - continue; - } + continue; + } - int.TryParse(t.TvDbId, out var tvdbId); - var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId); - if (info == null) - { - 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)) { @@ -752,7 +753,7 @@ namespace Ombi.Schedule.Jobs.Ombi { AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); } - + } catch (Exception e) { diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs b/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs index 586bd75e9..9978b7e2b 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs @@ -79,11 +79,16 @@ namespace Ombi.Schedule.Jobs.Plex { seriesEpisodes = plexEpisodes.Where(x => x.Series.ImdbId == imdbId.ToString()); } - if (useTvDb) + if (useTvDb && (seriesEpisodes == null || !seriesEpisodes.Any()) ) { seriesEpisodes = plexEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString()); } + if (seriesEpisodes == null) + { + continue; + } + if (!seriesEpisodes.Any()) { // Let's try and match the series by name diff --git a/src/Ombi.Settings/Ombi.Settings.csproj b/src/Ombi.Settings/Ombi.Settings.csproj index 3cb56cb07..19a415a47 100644 --- a/src/Ombi.Settings/Ombi.Settings.csproj +++ b/src/Ombi.Settings/Ombi.Settings.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs b/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs index 3faba3e42..8fc8111f7 100644 --- a/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Ombi.Settings.Settings.Models.External; namespace Ombi.Core.Settings.Models.External @@ -6,6 +7,10 @@ namespace Ombi.Core.Settings.Models.External public sealed class PlexSettings : Ombi.Settings.Settings.Models.Settings { public bool Enable { get; set; } + /// + /// This is the ClientId for OAuth + /// + public Guid InstallId { get; set; } public List Servers { get; set; } } diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index e9937509e..8d6036877 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -183,7 +183,7 @@ namespace Ombi.Store.Context notificationToAdd = new NotificationTemplates { NotificationType = notificationType, - Message = "Hello! You {Title} on {ApplicationName}! This is now available! :)", + Message = "Hello! Your request for {Title} on {ApplicationName}! This is now available! :)", Subject = "{ApplicationName}: {Title} is now available!", Agent = agent, Enabled = true, diff --git a/src/Ombi.Store/Migrations/20180703200952_EmbyUrlFix.Designer.cs b/src/Ombi.Store/Migrations/20180703200952_EmbyUrlFix.Designer.cs new file mode 100644 index 000000000..cebb7d22e --- /dev/null +++ b/src/Ombi.Store/Migrations/20180703200952_EmbyUrlFix.Designer.cs @@ -0,0 +1,981 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Ombi.Helpers; +using Ombi.Store.Context; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using System; + +namespace Ombi.Store.Migrations +{ + [DbContext(typeof(OmbiContext))] + [Migration("20180703200952_EmbyUrlFix")] + partial class EmbyUrlFix + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.3-rtm-10026"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Type"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("ApplicationConfiguration"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuditArea"); + + b.Property("AuditType"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("User"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId") + .IsRequired(); + + b.Property("ImdbId"); + + b.Property("ProviderId"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId"); + + b.Property("EpisodeNumber"); + + b.Property("ImdbId"); + + b.Property("ParentId"); + + b.Property("ProviderId"); + + b.Property("SeasonNumber"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.Property("SettingsName"); + + b.HasKey("Id"); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("Message"); + + b.Property("NotificationType"); + + b.Property("Subject"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("PlayerId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("Alias"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("EmbyConnectUserId"); + + b.Property("EpisodeRequestLimit"); + + b.Property("LastLoggedIn"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("MovieRequestLimit"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("ProviderUserId"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserAccessToken"); + + b.Property("UserName") + .HasMaxLength(256); + + b.Property("UserType"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ParentKey"); + + b.Property("PlexContentId"); + + b.Property("PlexServerContentId"); + + b.Property("SeasonKey"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ImdbId"); + + b.Property("Key"); + + b.Property("Quality"); + + b.Property("ReleaseYear"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("HasFile"); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ContentId"); + + b.Property("ContentType"); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("IssueId"); + + b.Property("ParentRequestId"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("SeriesType"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Comment"); + + b.Property("Date"); + + b.Property("IssuesId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueCategoryId"); + + b.Property("IssueId"); + + b.Property("ProviderId"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("ResovledDate"); + + b.Property("Status"); + + b.Property("Subject"); + + b.Property("Title"); + + b.Property("UserReportedId"); + + b.HasKey("Id"); + + b.HasIndex("IssueCategoryId"); + + b.HasIndex("IssueId"); + + b.HasIndex("UserReportedId"); + + b.ToTable("Issues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Background"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("DigitalReleaseDate"); + + b.Property("ImdbId"); + + b.Property("IssueId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("RootPathOverride"); + + b.Property("Status"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeCount"); + + b.Property("RequestDate"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Background"); + + b.Property("ImdbId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RootFolder"); + + b.Property("Status"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestSubscription"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("HasFile"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Token"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AirDate"); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("EpisodeNumber"); + + b.Property("Requested"); + + b.Property("SeasonId"); + + b.Property("Title"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChildRequestId"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany("NotificationUserIds") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent") + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest") + .WithMany("ChildRequests") + .HasForeignKey("ParentRequestId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues") + .WithMany("Comments") + .HasForeignKey("IssuesId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory") + .WithMany() + .HasForeignKey("IssueCategoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") + .WithMany() + .HasForeignKey("UserReportedId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest") + .WithMany("SeasonRequests") + .HasForeignKey("ChildRequestId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/20180703200952_EmbyUrlFix.cs b/src/Ombi.Store/Migrations/20180703200952_EmbyUrlFix.cs new file mode 100644 index 000000000..97e714a65 --- /dev/null +++ b/src/Ombi.Store/Migrations/20180703200952_EmbyUrlFix.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Ombi.Store.Migrations +{ + public partial class EmbyUrlFix : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @"UPDATE EmbyContent SET Url = replace( Url, 'http://app.emby.media/itemdetails.html', 'http://app.emby.media/#!/itemdetails.html' ) WHERE Url LIKE 'http://app.emby.media/itemdetails.html%';"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Ombi.Store/Ombi.Store.csproj b/src/Ombi.Store/Ombi.Store.csproj index 703bbf672..2ceb78424 100644 --- a/src/Ombi.Store/Ombi.Store.csproj +++ b/src/Ombi.Store/Ombi.Store.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/Ombi.Tests/Ombi.Tests.csproj b/src/Ombi.Tests/Ombi.Tests.csproj index 6296d9e8c..95eb09353 100644 --- a/src/Ombi.Tests/Ombi.Tests.csproj +++ b/src/Ombi.Tests/Ombi.Tests.csproj @@ -7,12 +7,12 @@ - + - + diff --git a/src/Ombi.Updater/Ombi.Updater.csproj b/src/Ombi.Updater/Ombi.Updater.csproj index 3a55a7040..07fb92d81 100644 --- a/src/Ombi.Updater/Ombi.Updater.csproj +++ b/src/Ombi.Updater/Ombi.Updater.csproj @@ -3,7 +3,7 @@ Exe win10-x64;win10-x86;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;linux-arm;linux-arm64; - netcoreapp2.0 + netcoreapp2.1 3.0.0.0 3.0.0.0 @@ -12,14 +12,14 @@ - - - - - - - - + + + + + + + + diff --git a/src/Ombi/ApiKeyMiddlewear.cs b/src/Ombi/ApiKeyMiddlewear.cs index f38317b3e..f3c956df4 100644 --- a/src/Ombi/ApiKeyMiddlewear.cs +++ b/src/Ombi/ApiKeyMiddlewear.cs @@ -94,9 +94,31 @@ namespace Ombi } else { - var identity = new GenericIdentity("API"); - var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" }); - context.User = principal; + // Check if we have a UserName header if so we can impersonate that user + if (context.Request.Headers.Keys.Contains("UserName", StringComparer.InvariantCultureIgnoreCase)) + { + var username = context.Request.Headers["UserName"].FirstOrDefault(); + var um = context.RequestServices.GetService(); + var user = await um.Users.FirstOrDefaultAsync(x => + x.UserName.Equals(username, StringComparison.InvariantCultureIgnoreCase)); + if (user == null) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + await context.Response.WriteAsync("Invalid User"); + await next.Invoke(context); + } + var roles = await um.GetRolesAsync(user); + var identity = new GenericIdentity(user.UserName); + var principal = new GenericPrincipal(identity, roles.ToArray()); + context.User = principal; + } + else + { + var identity = new GenericIdentity("API"); + var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" }); + context.User = principal; + } + await next.Invoke(context); } } diff --git a/src/Ombi/ClientApp/app/app.module.ts b/src/Ombi/ClientApp/app/app.module.ts index 1421b1117..257a07cd0 100644 --- a/src/Ombi/ClientApp/app/app.module.ts +++ b/src/Ombi/ClientApp/app/app.module.ts @@ -36,7 +36,7 @@ import { ImageService } from "./services"; import { LandingPageService } from "./services"; import { NotificationService } from "./services"; import { SettingsService } from "./services"; -import { IssuesService, JobService, StatusService } from "./services"; +import { IssuesService, JobService, PlexTvService, StatusService } from "./services"; const routes: Routes = [ { path: "*", component: PageNotFoundComponent }, @@ -133,6 +133,7 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo CookieService, JobService, IssuesService, + PlexTvService, ], bootstrap: [AppComponent], }) diff --git a/src/Ombi/ClientApp/app/auth/IUserLogin.ts b/src/Ombi/ClientApp/app/auth/IUserLogin.ts index 609055c8e..d0e4d374a 100644 --- a/src/Ombi/ClientApp/app/auth/IUserLogin.ts +++ b/src/Ombi/ClientApp/app/auth/IUserLogin.ts @@ -1,8 +1,11 @@ -export interface IUserLogin { +import { IPlexPin } from "../interfaces"; + +export interface IUserLogin { username: string; password: string; rememberMe: boolean; usePlexOAuth: boolean; + plexTvPin: IPlexPin; } export interface ILocalUser { diff --git a/src/Ombi/ClientApp/app/interfaces/IPlex.ts b/src/Ombi/ClientApp/app/interfaces/IPlex.ts index 823b80d32..ccc4e0300 100644 --- a/src/Ombi/ClientApp/app/interfaces/IPlex.ts +++ b/src/Ombi/ClientApp/app/interfaces/IPlex.ts @@ -2,6 +2,16 @@ user: IPlexUser; } +export interface IPlexPin { + id: number; + code: string; +} + +export interface IPlexOAuthViewModel { + wizard: boolean; + pin: IPlexPin; +} + export interface IPlexOAuthAccessToken { accessToken: string; } diff --git a/src/Ombi/ClientApp/app/login/login.component.ts b/src/Ombi/ClientApp/app/login/login.component.ts index 3447f84c3..7a2a605ce 100644 --- a/src/Ombi/ClientApp/app/login/login.component.ts +++ b/src/Ombi/ClientApp/app/login/login.component.ts @@ -6,7 +6,7 @@ import { TranslateService } from "@ngx-translate/core"; import { PlatformLocation } from "@angular/common"; import { AuthService } from "../auth/auth.service"; import { IAuthenticationSettings, ICustomizationSettings } from "../interfaces"; -import { NotificationService } from "../services"; +import { NotificationService, PlexTvService } from "../services"; import { SettingsService } from "../services"; import { StatusService } from "../services"; @@ -40,13 +40,14 @@ export class LoginComponent implements OnDestroy, OnInit { } private timer: any; + private clientId: string; private errorBody: string; private errorValidation: string; constructor(private authService: AuthService, private router: Router, private notify: NotificationService, private status: StatusService, private fb: FormBuilder, private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer, - private route: ActivatedRoute, private location: PlatformLocation, private readonly translate: TranslateService) { + private route: ActivatedRoute, private location: PlatformLocation, private translate: TranslateService, private plexTv: PlexTvService) { this.route.params .subscribe((params: any) => { this.landingFlag = params.landing; @@ -78,6 +79,7 @@ export class LoginComponent implements OnDestroy, OnInit { public ngOnInit() { this.settingsService.getAuthentication().subscribe(x => this.authenticationSettings = x); + this.settingsService.getClientId().subscribe(x => this.clientId = x); this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); this.images.getRandomBackground().subscribe(x => { this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%),url(" + x.url + ")"); @@ -101,7 +103,7 @@ export class LoginComponent implements OnDestroy, OnInit { return; } const value = form.value; - const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false }; + const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false, plexTvPin: { id: 0, code: ""} }; this.authService.requiresPassword(user).subscribe(x => { if(x && this.authenticationSettings.allowNoPassword) { // Looks like this user requires a password @@ -123,14 +125,17 @@ export class LoginComponent implements OnDestroy, OnInit { } public oauth() { - this.authService.login({usePlexOAuth: true, password:"",rememberMe:true,username:""}).subscribe(x => { - if (window.frameElement) { - // in frame - window.open(x.url, "_blank"); - } else { - // not in frame - window.location.href = x.url; - } + this.plexTv.GetPin(this.clientId, this.appName).subscribe(pin => { + + this.authService.login({usePlexOAuth: true, password:"",rememberMe:true,username:"", plexTvPin: pin}).subscribe(x => { + if (window.frameElement) { + // in frame + window.open(x.url, "_blank"); + } else { + // not in frame + window.location.href = x.url; + } + }); }); } diff --git a/src/Ombi/ClientApp/app/services/applications/index.ts b/src/Ombi/ClientApp/app/services/applications/index.ts index 98c61cf04..9fe4a5403 100644 --- a/src/Ombi/ClientApp/app/services/applications/index.ts +++ b/src/Ombi/ClientApp/app/services/applications/index.ts @@ -5,3 +5,4 @@ export * from "./radarr.service"; export * from "./sonarr.service"; export * from "./tester.service"; export * from "./plexoauth.service"; +export * from "./plextv.service"; diff --git a/src/Ombi/ClientApp/app/services/applications/plex.service.ts b/src/Ombi/ClientApp/app/services/applications/plex.service.ts index 53fd31f9d..296f79ddb 100644 --- a/src/Ombi/ClientApp/app/services/applications/plex.service.ts +++ b/src/Ombi/ClientApp/app/services/applications/plex.service.ts @@ -6,7 +6,7 @@ import { Observable } from "rxjs/Rx"; import { ServiceHelpers } from "../service.helpers"; -import { IPlexAuthentication, IPlexLibResponse, IPlexServer, IPlexServerViewModel, IUsersModel } from "../../interfaces"; +import { IPlexAuthentication, IPlexLibResponse, IPlexOAuthViewModel,IPlexServer, IPlexServerViewModel, IUsersModel } from "../../interfaces"; @Injectable() export class PlexService extends ServiceHelpers { @@ -30,7 +30,7 @@ export class PlexService extends ServiceHelpers { return this.http.get(`${this.url}Friends`, {headers: this.headers}); } - public oAuth(wizard: boolean): Observable { - return this.http.get(`${this.url}oauth/${wizard}`, {headers: this.headers}); + public oAuth(wizard: IPlexOAuthViewModel): Observable { + return this.http.post(`${this.url}oauth`, JSON.stringify(wizard), {headers: this.headers}); } } diff --git a/src/Ombi/ClientApp/app/services/applications/plextv.service.ts b/src/Ombi/ClientApp/app/services/applications/plextv.service.ts new file mode 100644 index 000000000..58a756007 --- /dev/null +++ b/src/Ombi/ClientApp/app/services/applications/plextv.service.ts @@ -0,0 +1,28 @@ +import { PlatformLocation } from "@angular/common"; +import { HttpClient, HttpHeaders } from "@angular/common/http"; +import { Injectable } from "@angular/core"; + +import { Observable } from "rxjs/Rx"; + +import { IPlexPin } from "../../interfaces"; + +@Injectable() +export class PlexTvService { + + constructor(private http: HttpClient, public platformLocation: PlatformLocation) { + + } + + public GetPin(clientId: string, applicationName: string): Observable { + const headers = new HttpHeaders({"Content-Type":"application/json", + "X-Plex-Client-Identifier": clientId, + "X-Plex-Product": applicationName, + "X-Plex-Version": "3", + "X-Plex-Device": "Ombi (Web)", + "X-Plex-Platform": "Web", + "Accept": "application/json", + }); + return this.http.post("https://plex.tv/api/v2/pins?strong=true", null, {headers}); + } + +} diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index 726c86d63..ff49e393c 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -95,6 +95,10 @@ export class SettingsService extends ServiceHelpers { return this.http.get(`${this.url}/Authentication`, {headers: this.headers}); } + public getClientId(): Observable { + return this.http.get(`${this.url}/clientid`, {headers: this.headers}); + } + public saveAuthentication(settings: IAuthenticationSettings): Observable { return this.http.post(`${this.url}/Authentication`, JSON.stringify(settings), {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts index 74ad37e12..1e1109ea4 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts @@ -80,10 +80,10 @@ export class UserManagementComponent implements OnInit { if(anyRoles) { x.claims = this.availableClaims; } - if(this.bulkEpisodeLimit && this.bulkEpisodeLimit > 0) { + if(this.bulkEpisodeLimit) { x.episodeRequestLimit = this.bulkEpisodeLimit; } - if(this.bulkMovieLimit && this.bulkMovieLimit > 0) { + if(this.bulkMovieLimit) { x.movieRequestLimit = this.bulkMovieLimit; } this.identityService.updateUser(x).subscribe(y => { diff --git a/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts b/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts index 67bd672dc..81b5db0b4 100644 --- a/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts +++ b/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts @@ -1,20 +1,27 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { PlexService } from "../../services"; +import { PlexService, PlexTvService, SettingsService } from "../../services"; import { IdentityService, NotificationService } from "../../services"; @Component({ templateUrl: "./plex.component.html", }) -export class PlexComponent { +export class PlexComponent implements OnInit { public login: string; public password: string; + private clientId: string; + constructor(private plexService: PlexService, private router: Router, private notificationService: NotificationService, - private identityService: IdentityService) { } + private identityService: IdentityService, private plexTv: PlexTvService, + private settingsService: SettingsService) { } + + public ngOnInit(): void { + this.settingsService.getClientId().subscribe(x => this.clientId = x); + } public requestAuthToken() { this.plexService.logIn(this.login, this.password).subscribe(x => { @@ -40,10 +47,12 @@ export class PlexComponent { } public oauth() { - this.plexService.oAuth(true).subscribe(x => { - if(x.url) { - window.location.href = x.url; - } + this.plexTv.GetPin(this.clientId, "Ombi").subscribe(pin => { + this.plexService.oAuth({wizard: true, pin}).subscribe(x => { + if(x.url) { + window.location.href = x.url; + } + }); }); } } diff --git a/src/Ombi/Controllers/External/PlexController.cs b/src/Ombi/Controllers/External/PlexController.cs index 4819ed8a0..6ea37e9cc 100644 --- a/src/Ombi/Controllers/External/PlexController.cs +++ b/src/Ombi/Controllers/External/PlexController.cs @@ -12,6 +12,7 @@ using Ombi.Core.Authentication; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; +using Ombi.Models; using Ombi.Models.External; namespace Ombi.Controllers.External @@ -177,25 +178,23 @@ namespace Ombi.Controllers.External return vm.DistinctBy(x => x.Id); } - [HttpGet("oauth/{wizard:bool}")] + [HttpPost("oauth")] [AllowAnonymous] - public async Task OAuth(bool wizard) + public async Task OAuth([FromBody]PlexOAuthViewModel wizard) { //https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd // Plex OAuth // Redirect them to Plex - // We need a PIN first - var pin = await _plexOAuthManager.RequestPin(); Uri url; - if (!wizard) + if (!wizard.Wizard) { - url = await _plexOAuthManager.GetOAuthUrl(pin.id, pin.code); + url = await _plexOAuthManager.GetOAuthUrl(wizard.Pin.id, wizard.Pin.code); } else { var websiteAddress =$"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}"; - url = _plexOAuthManager.GetWizardOAuthUrl(pin.id, pin.code, websiteAddress); + url = await _plexOAuthManager.GetWizardOAuthUrl(wizard.Pin.id, wizard.Pin.code, websiteAddress); } if (url == null) diff --git a/src/Ombi/Controllers/IdentityController.cs b/src/Ombi/Controllers/IdentityController.cs index e9fb17af6..38c2d1e94 100644 --- a/src/Ombi/Controllers/IdentityController.cs +++ b/src/Ombi/Controllers/IdentityController.cs @@ -57,7 +57,9 @@ namespace Ombi.Controllers ISettingsService settings, IRepository requestLog, IRepository issues, - IRepository issueComments) + IRepository issueComments, + IRepository notificationRepository, + IRepository subscriptionRepository) { UserManager = user; Mapper = mapper; @@ -75,6 +77,8 @@ namespace Ombi.Controllers _requestLogRepository = requestLog; _issueCommentsRepository = issueComments; OmbiSettings = ombiSettings; + _requestSubscriptionRepository = subscriptionRepository; + _notificationRepository = notificationRepository; } private OmbiUserManager UserManager { get; } @@ -93,6 +97,8 @@ namespace Ombi.Controllers private readonly IRepository _issuesRepository; private readonly IRepository _issueCommentsRepository; private readonly IRepository _requestLogRepository; + private readonly IRepository _notificationRepository; + private readonly IRepository _requestSubscriptionRepository; /// @@ -568,6 +574,19 @@ namespace Ombi.Controllers { await _issueCommentsRepository.DeleteRange(issueComments); } + + // Delete the Subscriptions and mobile notification ids + var subs = _requestSubscriptionRepository.GetAll().Where(x => x.UserId == userId); + var mobileIds = _notificationRepository.GetAll().Where(x => x.UserId == userId); + if (subs.Any()) + { + await _requestSubscriptionRepository.DeleteRange(subs); + } + + if (mobileIds.Any()) + { + await _notificationRepository.DeleteRange(mobileIds); + } var result = await UserManager.DeleteAsync(userToDelete); if (result.Succeeded) diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index 895621ad3..50938942b 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -127,10 +127,23 @@ namespace Ombi.Controllers public async Task PlexSettings() { var s = await Get(); - return s; } + [HttpGet("clientid")] + [AllowAnonymous] + public async Task GetClientId() + { + var s = await Get(); + if (s.InstallId == Guid.Empty) + { + s.InstallId = Guid.NewGuid(); + // Save it + await PlexSettings(s); + } + return s.InstallId.ToString("N"); + } + /// /// Save the Plex settings. /// @@ -139,6 +152,10 @@ namespace Ombi.Controllers [HttpPost("plex")] public async Task PlexSettings([FromBody]PlexSettings plex) { + if (plex.InstallId == null || plex.InstallId == Guid.Empty) + { + plex.InstallId = Guid.NewGuid(); + } var result = await Save(plex); return result; } diff --git a/src/Ombi/Controllers/TokenController.cs b/src/Ombi/Controllers/TokenController.cs index b45752af4..aad367dbe 100644 --- a/src/Ombi/Controllers/TokenController.cs +++ b/src/Ombi/Controllers/TokenController.cs @@ -80,12 +80,10 @@ namespace Ombi.Controllers { // Plex OAuth // Redirect them to Plex - // We need a PIN first - var pin = await _plexOAuthManager.RequestPin(); - + var websiteAddress = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}"; //https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd - var url = await _plexOAuthManager.GetOAuthUrl(pin.id, pin.code, websiteAddress); + var url = await _plexOAuthManager.GetOAuthUrl(model.PlexTvPin.id, model.PlexTvPin.code, websiteAddress); if (url == null) { return new JsonResult(new diff --git a/src/Ombi/Models/PlexOAuthViewModel.cs b/src/Ombi/Models/PlexOAuthViewModel.cs new file mode 100644 index 000000000..dfc2eda8f --- /dev/null +++ b/src/Ombi/Models/PlexOAuthViewModel.cs @@ -0,0 +1,10 @@ +using Ombi.Api.Plex.Models.OAuth; + +namespace Ombi.Models +{ + public class PlexOAuthViewModel + { + public bool Wizard { get; set; } + public OAuthPin Pin { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi/Models/UserAuthModel.cs b/src/Ombi/Models/UserAuthModel.cs index 046119821..5f240699b 100644 --- a/src/Ombi/Models/UserAuthModel.cs +++ b/src/Ombi/Models/UserAuthModel.cs @@ -1,4 +1,6 @@ -namespace Ombi.Models +using Ombi.Api.Plex.Models.OAuth; + +namespace Ombi.Models { public class UserAuthModel { @@ -7,5 +9,6 @@ public bool RememberMe { get; set; } public bool UsePlexAdminAccount { get; set; } public bool UsePlexOAuth { get; set; } + public OAuthPin PlexTvPin { get; set; } } } \ No newline at end of file diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 8b9e00c45..9212b3f79 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp2.1 win10-x64;win10-x86;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;linux-arm;linux-arm64; false Latest @@ -67,10 +67,10 @@ - - - - + + + + @@ -78,7 +78,7 @@ - + diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 7e11cfe0e..42503a5c6 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -127,6 +127,12 @@ namespace Ombi //x.UseConsole(); }); + services.AddCors(o => o.AddPolicy("MyPolicy", builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + })); // Build the intermediate service provider return services.BuildServiceProvider(); @@ -209,13 +215,16 @@ namespace Ombi app.UseMiddleware(); app.UseMiddleware(); + app.UseCors("MyPolicy"); //app.ApiKeyMiddlewear(app.ApplicationServices); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); - + + + app.UseMvc(routes => { routes.MapRoute( diff --git a/src/Ombi/StartupExtensions.cs b/src/Ombi/StartupExtensions.cs index a2c44e7f3..d81227ff4 100644 --- a/src/Ombi/StartupExtensions.cs +++ b/src/Ombi/StartupExtensions.cs @@ -1,29 +1,38 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net; using System.Reflection; -using System.Security.Principal; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.PlatformAbstractions; using Microsoft.IdentityModel.Tokens; using Ombi.Config; -using Ombi.Core.Authentication; -using Ombi.Core.Settings; using Ombi.Helpers; using Ombi.Models.Identity; -using Ombi.Settings.Settings.Models; using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Ombi { + public class AddRequiredHeaderParameter : IOperationFilter + { + + public void Apply(Operation operation, OperationFilterContext context) + { + if (operation.Parameters == null) + operation.Parameters = new List(); + + operation.Parameters.Add(new NonBodyParameter + { + Name = "ApiKey", + In = "header", + Type = "apiKey", + + }); + } + } public static class StartupExtensions { public static void AddSwagger(this IServiceCollection services) @@ -35,17 +44,37 @@ namespace Ombi { Version = "v1", Title = "Ombi Api", - Description = "The API for Ombi, most of these calls require an auth token that you can get from calling POST:\"/api/v1/token\" with the body of: \n {\n\"username\":\"YOURUSERNAME\",\n\"password\":\"YOURPASSWORD\"\n} \n" + - "You can then use the returned token in the JWT Token field e.g. \"Bearer Token123xxff\"", Contact = new Contact { - Email = "tidusjar@gmail.com", Name = "Jamie Rees", Url = "https://www.ombi.io/" } }); + var security = new Dictionary> + { + //{"Bearer", new string[] { }}, + {"ApiKey", new string[] { }}, + }; + + //c.AddSecurityDefinition("Bearer", new ApiKeyScheme + //{ + // Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + // Name = "Authorization", + // In = "header", + // Type = "apiKey" + //}); + + c.AddSecurityDefinition("ApiKey", new ApiKeyScheme + { + Description = "API Key provided by Ombi. Example: \"ApiKey: {token}\"", + Name = "ApiKey", + In = "header", + Type = "apiKey" + }); + c.AddSecurityRequirement(security); c.CustomSchemaIds(x => x.FullName); - var basePath = PlatformServices.Default.Application.ApplicationBasePath; + c.OperationFilter(); + var basePath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); var xmlPath = Path.Combine(basePath, "Swagger.xml"); try { @@ -55,13 +84,7 @@ namespace Ombi { Console.WriteLine(e); } - c.AddSecurityDefinition("Bearer", new JwtBearer - { - Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", - Name = "Authorization", - In = "header", - Type = "apiKey", - }); + c.OperationFilter(); c.DescribeAllParametersInCamelCase();