diff --git a/PlexRequests.Api.Interfaces/IPlexApi.cs b/PlexRequests.Api.Interfaces/IPlexApi.cs index baad57aad..52d598aba 100644 --- a/PlexRequests.Api.Interfaces/IPlexApi.cs +++ b/PlexRequests.Api.Interfaces/IPlexApi.cs @@ -43,5 +43,6 @@ namespace PlexRequests.Api.Interfaces PlexEpisodeMetadata GetEpisodeMetaData(string authToken, Uri host, string ratingKey); PlexSearch GetAllEpisodes(string authToken, Uri host, string section, int startPage, int returnCount); PlexServer GetServer(string authToken); + PlexMetadata GetSeasons(string authToken, Uri plexFullHost, string ratingKey); } } \ No newline at end of file diff --git a/PlexRequests.Api.Models/Plex/PlexSearch.cs b/PlexRequests.Api.Models/Plex/PlexSearch.cs index c66c62802..541f64918 100644 --- a/PlexRequests.Api.Models/Plex/PlexSearch.cs +++ b/PlexRequests.Api.Models/Plex/PlexSearch.cs @@ -311,6 +311,7 @@ namespace PlexRequests.Api.Models.Plex public string UpdatedAt { get; set; } [XmlAttribute(AttributeName = "parentTitle")] public string ParentTitle { get; set; } + public List Seasons { get; set; } } diff --git a/PlexRequests.Api/PlexApi.cs b/PlexRequests.Api/PlexApi.cs index d4ffa450f..a16402680 100644 --- a/PlexRequests.Api/PlexApi.cs +++ b/PlexRequests.Api/PlexApi.cs @@ -301,6 +301,36 @@ namespace PlexRequests.Api } } + + public PlexMetadata GetSeasons(string authToken, Uri plexFullHost, string ratingKey) + { + var request = new RestRequest + { + Method = Method.GET, + Resource = "library/metadata/{ratingKey}/children" + }; + + request.AddUrlSegment("ratingKey", ratingKey); + AddHeaders(ref request, authToken); + + try + { + var lib = RetryHandler.Execute(() => Api.ExecuteXml(request, plexFullHost), + (exception, timespan) => Log.Error(exception, "Exception when calling GetMetadata for Plex, Retrying {0}", timespan), new[] { + TimeSpan.FromSeconds (5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }); + + return lib; + } + catch (Exception e) + { + Log.Error(e, "There has been a API Exception when attempting to get the Plex GetMetadata"); + return new PlexMetadata(); + } + } + public PlexServer GetServer(string authToken) { var request = new RestRequest diff --git a/PlexRequests.Api/TheMovieDbApi.cs b/PlexRequests.Api/TheMovieDbApi.cs index ce0d3772c..8137ad05f 100644 --- a/PlexRequests.Api/TheMovieDbApi.cs +++ b/PlexRequests.Api/TheMovieDbApi.cs @@ -1,84 +1,84 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: TheMovieDbApi.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.Threading.Tasks; - -using TMDbLib.Client; -using TMDbLib.Objects.General; -using TMDbLib.Objects.Movies; -using TMDbLib.Objects.Search; -using TMDbLib.Objects.TvShows; - -namespace PlexRequests.Api -{ - public class TheMovieDbApi : MovieBase - { - public TheMovieDbApi() - { - Client = new TMDbClient(ApiKey); - } - - public TMDbClient Client { get; set; } - public async Task> SearchMovie(string searchTerm) - { - var results = await Client.SearchMovie(searchTerm); - return results.Results; - } - - [Obsolete("Should use TvMaze for TV")] - public async Task> SearchTv(string searchTerm) - { - var results = await Client.SearchTvShow(searchTerm); - return results.Results; - } - - public async Task> GetCurrentPlayingMovies() - { - var movies = await Client.GetMovieList(MovieListType.NowPlaying); - return movies.Results; - } - public async Task> GetUpcomingMovies() - { - var movies = await Client.GetMovieList(MovieListType.Upcoming); - return movies.Results; - } - - public async Task GetMovieInformation(int tmdbId) - { - var movies = await Client.GetMovie(tmdbId); - return movies; - } - - [Obsolete("Should use TvMaze for TV")] - public async Task GetTvShowInformation(int tmdbId) - { - var show = await Client.GetTvShow(tmdbId); - return show; - } - } -} +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: TheMovieDbApi.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.Threading.Tasks; + +using TMDbLib.Client; +using TMDbLib.Objects.General; +using TMDbLib.Objects.Movies; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; + +namespace PlexRequests.Api +{ + public class TheMovieDbApi : MovieBase + { + public TheMovieDbApi() + { + Client = new TMDbClient(ApiKey); + } + + public TMDbClient Client { get; set; } + public async Task> SearchMovie(string searchTerm) + { + var results = await Client.SearchMovie(searchTerm); + return results.Results; + } + + [Obsolete("Should use TvMaze for TV")] + public async Task> SearchTv(string searchTerm) + { + var results = await Client.SearchTvShow(searchTerm); + return results.Results; + } + + public async Task> GetCurrentPlayingMovies() + { + var movies = await Client.GetMovieList(MovieListType.NowPlaying); + return movies.Results; + } + public async Task> GetUpcomingMovies() + { + var movies = await Client.GetMovieList(MovieListType.Upcoming); + return movies.Results; + } + + public async Task GetMovieInformation(int tmdbId) + { + var movies = await Client.GetMovie(tmdbId); + return movies; + } + + [Obsolete("Should use TvMaze for TV")] + public async Task GetTvShowInformation(int tmdbId) + { + var show = await Client.GetTvShow(tmdbId); + return show; + } + } +} diff --git a/PlexRequests.Helpers.Tests/PlexHelperTests.cs b/PlexRequests.Helpers.Tests/PlexHelperTests.cs index 178ed4652..6f09d6c39 100644 --- a/PlexRequests.Helpers.Tests/PlexHelperTests.cs +++ b/PlexRequests.Helpers.Tests/PlexHelperTests.cs @@ -55,6 +55,11 @@ namespace PlexRequests.Helpers.Tests return list; } + [TestCaseSource(nameof(SeasonNumbers))] + public int TitleToSeasonNumber(string title) + { + return PlexHelper.GetSeasonNumberFromTitle(title); + } private static IEnumerable PlexGuids { @@ -70,6 +75,23 @@ namespace PlexRequests.Helpers.Tests } } + private static IEnumerable SeasonNumbers + { + get + { + yield return new TestCaseData("Season 1").Returns(1).SetName("Season 1"); + yield return new TestCaseData("Season 2").Returns(2).SetName("Season 2"); + yield return new TestCaseData("Season 3").Returns(3).SetName("Season 3"); + yield return new TestCaseData("Season 4").Returns(4).SetName("Season 4"); + yield return new TestCaseData("Season 5").Returns(5).SetName("Season 5"); + yield return new TestCaseData("Season 100").Returns(100).SetName("Season 100"); + yield return new TestCaseData("InvalidSeason").Returns(0).SetName("InvalidSeason"); + yield return new TestCaseData("Invalid Season with no number").Returns(0).SetName("Invalid Season with no number"); + yield return new TestCaseData("").Returns(0).SetName("Empty string"); + yield return new TestCaseData(null).Returns(0).SetName("Null string"); + } + } + private static IEnumerable PlexTvEpisodeGuids { get diff --git a/PlexRequests.Helpers.Tests/app.config b/PlexRequests.Helpers.Tests/app.config index b34d39119..cb3809383 100644 --- a/PlexRequests.Helpers.Tests/app.config +++ b/PlexRequests.Helpers.Tests/app.config @@ -4,7 +4,7 @@ - + diff --git a/PlexRequests.Helpers/PlexHelper.cs b/PlexRequests.Helpers/PlexHelper.cs index 0017d9fbf..baafcd451 100644 --- a/PlexRequests.Helpers/PlexHelper.cs +++ b/PlexRequests.Helpers/PlexHelper.cs @@ -72,6 +72,29 @@ namespace PlexRequests.Helpers return ep; } } + + public static int GetSeasonNumberFromTitle(string title) + { + if (string.IsNullOrEmpty(title)) + { + return 0; + } + + var split = title.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (split.Length < 2) + { + // Cannot get the season number, it's not in the usual format + return 0; + } + + int season; + if (int.TryParse(split[1], out season)) + { + return season; + } + + return 0; + } } public class EpisodeModelHelper diff --git a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs index cbab32602..8992e6545 100644 --- a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs +++ b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs @@ -38,7 +38,7 @@ namespace PlexRequests.Services.Interfaces List GetPlexMovies(); bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year, string providerId = null); List GetPlexTvShows(); - bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null); + bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null, int[] seasons = null); List GetPlexAlbums(); bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist); bool IsEpisodeAvailable(string theTvDbId, int season, int episode); diff --git a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs index 270ae7d0c..05d398a7d 100644 --- a/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs +++ b/PlexRequests.Services/Jobs/PlexAvailabilityChecker.cs @@ -209,25 +209,33 @@ namespace PlexRequests.Services.Jobs ).ToArray(); foreach (var lib in tvLibs) - { + { shows.AddRange(lib.Directory.Select(x => new PlexTvShow // shows are in the directory list { Title = x.Title, ReleaseYear = x.Year, ProviderId = x.ProviderId, + Seasons = x.Seasons.Select(d => PlexHelper.GetSeasonNumberFromTitle(d.Title)).ToArray() })); } } return shows; } - public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null) + public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null, int[] seasons = null) { var advanced = !string.IsNullOrEmpty(providerId); foreach (var show in plexShows) { if (advanced) { + if (seasons != null) + { + if (seasons.Any(season => show.Seasons.Contains(season))) + { + return true; + } + } if (!string.IsNullOrEmpty(show.ProviderId) && show.ProviderId.Equals(providerId, StringComparison.InvariantCultureIgnoreCase)) { @@ -366,6 +374,8 @@ namespace PlexRequests.Services.Jobs var currentItem = results[i].Directory[j]; var metaData = PlexApi.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri, currentItem.RatingKey); + var seasons = PlexApi.GetSeasons(plexSettings.PlexAuthToken, plexSettings.FullUri, currentItem.RatingKey); + results[i].Directory[j] = seasons.Directory; var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Directory.Guid); results[i].Directory[j].ProviderId = providerId; } diff --git a/PlexRequests.Services/Models/PlexTvShow.cs b/PlexRequests.Services/Models/PlexTvShow.cs index 43375f0ca..5ac629132 100644 --- a/PlexRequests.Services/Models/PlexTvShow.cs +++ b/PlexRequests.Services/Models/PlexTvShow.cs @@ -1,9 +1,10 @@ -namespace PlexRequests.Services.Models -{ - public class PlexTvShow - { - public string Title { get; set; } - public string ReleaseYear { get; set; } - public string ProviderId { get; set; } - } -} +namespace PlexRequests.Services.Models +{ + public class PlexTvShow + { + public string Title { get; set; } + public string ReleaseYear { get; set; } + public string ProviderId { get; set; } + public int[] Seasons { get; set; } + } +} diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index ccb14bdda..2c694045a 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -59,6 +59,7 @@ using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using TMDbLib.Objects.General; +using TMDbLib.Objects.Search; using Action = PlexRequests.Helpers.Analytics.Action; using EpisodesModel = PlexRequests.Store.EpisodesModel; @@ -190,7 +191,7 @@ namespace PlexRequests.UI.Modules case MovieSearchType.Search: var movies = await MovieApi.SearchMovie(searchTerm); apiMovies = movies.Select(x => - new MovieResult() + new MovieResult { Adult = x.Adult, BackdropPath = x.BackdropPath, @@ -594,41 +595,6 @@ namespace PlexRequests.UI.Modules } } - try - { - var shows = Checker.GetPlexTvShows(); - var providerId = string.Empty; - var plexSettings = await PlexService.GetSettingsAsync(); - if (plexSettings.AdvancedSearch) - { - providerId = showId.ToString(); - } - if (episodeRequest) - { - var cachedEpisodesTask = await Checker.GetEpisodes(); - var cachedEpisodes = cachedEpisodesTask.ToList(); - foreach (var d in difference) - { - if (cachedEpisodes.Any(x => x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && x.ProviderId == providerId)) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" }); - } - } - } - else - { - if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId)) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" }); - } - } - } - catch (Exception) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) }); - } - - var model = new RequestedModel { ProviderId = showInfo.externals?.thetvdb ?? 0, @@ -687,6 +653,41 @@ namespace PlexRequests.UI.Modules model.SeasonList = seasonsList.ToArray(); + try + { + var shows = Checker.GetPlexTvShows(); + var providerId = string.Empty; + var plexSettings = await PlexService.GetSettingsAsync(); + if (plexSettings.AdvancedSearch) + { + providerId = showId.ToString(); + } + if (episodeRequest) + { + var cachedEpisodesTask = await Checker.GetEpisodes(); + var cachedEpisodes = cachedEpisodesTask.ToList(); + foreach (var d in difference) + { + if (cachedEpisodes.Any(x => x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && x.ProviderId == providerId)) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" }); + } + } + } + else + { + if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId, model.SeasonList)) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" }); + } + } + } + catch (Exception) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) }); + } + + if (ShouldAutoApprove(RequestType.TvShow, settings)) { model.Approved = true;