diff --git a/Ombi.Api.Interfaces/ISonarrApi.cs b/Ombi.Api.Interfaces/ISonarrApi.cs index bce750901..06dd4ee75 100644 --- a/Ombi.Api.Interfaces/ISonarrApi.cs +++ b/Ombi.Api.Interfaces/ISonarrApi.cs @@ -56,5 +56,8 @@ SonarrAddSeries AddSeriesNew(int tvdbId, string title, int qualityId, bool seaso Series UpdateSeries(Series series, string apiKey, Uri baseUrl); SonarrSeasonSearchResult SearchForSeason(int seriesId, int seasonNumber, string apiKey, Uri baseUrl); SonarrSeriesSearchResult SearchForSeries(int seriesId, string apiKey, Uri baseUrl); + + + SonarrAddSeries AddSeries(SonarrAddSeries series, string apiKey, Uri baseUrl); } } \ No newline at end of file diff --git a/Ombi.Api/SonarrApi.cs b/Ombi.Api/SonarrApi.cs index 3cf8565e8..afbe74e72 100644 --- a/Ombi.Api/SonarrApi.cs +++ b/Ombi.Api/SonarrApi.cs @@ -148,6 +148,42 @@ public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool s return result; } + public SonarrAddSeries AddSeries(SonarrAddSeries series,string apiKey, Uri baseUrl) + { + + var request = new RestRequest + { + Resource = "/api/Series?", + Method = Method.POST + }; + + Log.Debug("Sonarr API Options:"); + Log.Debug(series.DumpJson()); + + request.AddHeader("X-Api-Key", apiKey); + request.AddJsonBody(series); + + SonarrAddSeries result; + try + { + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (2) + }); + + result = policy.Execute(() => Api.ExecuteJson(request, baseUrl)); + } + catch (JsonSerializationException jse) + { + Log.Error(jse); + var error = Api.ExecuteJson>(request, baseUrl); + var messages = error?.Select(x => x.errorMessage).ToList(); + messages?.ForEach(x => Log.Error(x)); + result = new SonarrAddSeries { ErrorMessages = messages }; + } + + return result; + } + public SonarrAddSeries AddSeriesNew(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, bool searchForMissingEpisodes = false) { var request = new RestRequest @@ -244,7 +280,18 @@ public List GetSeries(string apiKey, Uri baseUrl) TimeSpan.FromSeconds(5) }); - return policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + var series = policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + + // Remove the 'specials from the object' + foreach (var s in series) + { + var seasonToRemove = s.seasons.FirstOrDefault(x => x.seasonNumber == 0); + if (seasonToRemove != null) + { + s.seasons.Remove(seasonToRemove); + } + } + return series; } catch (Exception e) { @@ -266,7 +313,15 @@ public Series GetSeries(string seriesId, string apiKey, Uri baseUrl) Log.Error(exception, "Exception when calling GetSeries by ID for Sonarr, Retrying {0}", timespan)); - return policy.Execute(() => Api.ExecuteJson(request, baseUrl)); + var series = policy.Execute(() => Api.ExecuteJson(request, baseUrl)); + + // Remove the specials season + var toRemove = series.seasons.FirstOrDefault(x => x.seasonNumber == 0); + if (toRemove != null) + { + series.seasons.Remove(toRemove); + } + return series; } catch (Exception e) { diff --git a/Ombi.Core.Migration/Migrations/Version2200.cs b/Ombi.Core.Migration/Migrations/Version2200.cs index d52e1654b..588d305ed 100644 --- a/Ombi.Core.Migration/Migrations/Version2200.cs +++ b/Ombi.Core.Migration/Migrations/Version2200.cs @@ -94,7 +94,7 @@ private void UpdateRecentlyAdded(IDbConnection con) content.Add(new RecentlyAddedLog { AddedAt = DateTime.UtcNow, - ProviderId = ep.ProviderId + ProviderId = ep.RatingKey }); } diff --git a/Ombi.Core.Migration/Migrations/Version2210.cs b/Ombi.Core.Migration/Migrations/Version2210.cs new file mode 100644 index 000000000..94592174a --- /dev/null +++ b/Ombi.Core.Migration/Migrations/Version2210.cs @@ -0,0 +1,134 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Version1100.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.Data; +using NLog; +using Ombi.Core.SettingModels; +using Ombi.Store; +using Ombi.Store.Models; +using Ombi.Store.Models.Emby; +using Ombi.Store.Models.Plex; +using Ombi.Store.Repository; +using Quartz.Collection; + +namespace Ombi.Core.Migration.Migrations +{ + [Migration(22100, "v2.21.0.0")] + public class Version2210 : BaseMigration, IMigration + { + public Version2210(IRepository log, + IRepository content, IRepository plexEp, IRepository embyContent, IRepository embyEp) + { + Log = log; + PlexContent = content; + PlexEpisodes = plexEp; + EmbyContent = embyContent; + EmbyEpisodes = embyEp; + } + + public int Version => 22100; + private IRepository Log { get; } + private IRepository PlexContent { get; } + private IRepository PlexEpisodes { get; } + private IRepository EmbyContent { get; } + private IRepository EmbyEpisodes { get; } + + public void Start(IDbConnection con) + { + UpdateRecentlyAdded(con); + UpdateSchema(con, Version); + + } + + private void UpdateRecentlyAdded(IDbConnection con) + { + + //Delete the recently added table, lets start again + Log.DeleteAll("RecentlyAddedLog"); + + + + // Plex + var plexAllContent = PlexContent.GetAll(); + var content = new HashSet(); + foreach (var plexContent in plexAllContent) + { + if(plexContent.Type == PlexMediaType.Artist) continue; + content.Add(new RecentlyAddedLog + { + AddedAt = DateTime.UtcNow, + ProviderId = plexContent.ProviderId + }); + } + Log.BatchInsert(content, "RecentlyAddedLog"); + + var plexEpisodeses = PlexEpisodes.GetAll(); + content.Clear(); + foreach (var ep in plexEpisodeses) + { + content.Add(new RecentlyAddedLog + { + AddedAt = DateTime.UtcNow, + ProviderId = ep.RatingKey + }); + } + Log.BatchInsert(content, "RecentlyAddedLog"); + + // Emby + content.Clear(); + var embyContent = EmbyContent.GetAll(); + foreach (var plexContent in embyContent) + { + content.Add(new RecentlyAddedLog + { + AddedAt = DateTime.UtcNow, + ProviderId = plexContent.EmbyId + }); + } + Log.BatchInsert(content, "RecentlyAddedLog"); + + var embyEpisodes = EmbyEpisodes.GetAll(); + content.Clear(); + foreach (var ep in embyEpisodes) + { + content.Add(new RecentlyAddedLog + { + AddedAt = DateTime.UtcNow, + ProviderId = ep.EmbyId + }); + } + Log.BatchInsert(content, "RecentlyAddedLog"); + + + } + + + } +} diff --git a/Ombi.Core.Migration/Ombi.Core.Migration.csproj b/Ombi.Core.Migration/Ombi.Core.Migration.csproj index 06dfda84d..4c2272a03 100644 --- a/Ombi.Core.Migration/Ombi.Core.Migration.csproj +++ b/Ombi.Core.Migration/Ombi.Core.Migration.csproj @@ -69,6 +69,7 @@ + diff --git a/Ombi.Core/Ombi.Core.csproj b/Ombi.Core/Ombi.Core.csproj index 49c37b597..3ad547420 100644 --- a/Ombi.Core/Ombi.Core.csproj +++ b/Ombi.Core/Ombi.Core.csproj @@ -155,8 +155,9 @@ - - + + + diff --git a/Ombi.Core/TvSender.cs b/Ombi.Core/Tv/TvSender.cs similarity index 99% rename from Ombi.Core/TvSender.cs rename to Ombi.Core/Tv/TvSender.cs index 3f6d5f86e..f43910c94 100644 --- a/Ombi.Core/TvSender.cs +++ b/Ombi.Core/Tv/TvSender.cs @@ -87,6 +87,8 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetRootPath(model.RootFolderSelected, sonarrSettings); + + if (episodeRequest) { // Does series exist? diff --git a/Ombi.Core/TvSenderOld.cs b/Ombi.Core/Tv/TvSenderOld.cs similarity index 100% rename from Ombi.Core/TvSenderOld.cs rename to Ombi.Core/Tv/TvSenderOld.cs diff --git a/Ombi.Core/Tv/TvSenderV2.cs b/Ombi.Core/Tv/TvSenderV2.cs new file mode 100644 index 000000000..11a97f498 --- /dev/null +++ b/Ombi.Core/Tv/TvSenderV2.cs @@ -0,0 +1,302 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: TvSenderV2.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NLog; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.SickRage; +using Ombi.Api.Models.Sonarr; +using Ombi.Core.SettingModels; +using Ombi.Helpers; +using Ombi.Store; + +namespace Ombi.Core.Tv +{ + public class TvSenderV2 + { + public TvSenderV2(ISonarrApi sonarrApi, ISickRageApi srApi, ICacheProvider cache) + { + SonarrApi = sonarrApi; + SickrageApi = srApi; + Cache = cache; + } + private ISonarrApi SonarrApi { get; } + private ISickRageApi SickrageApi { get; } + private ICacheProvider Cache { get; } + private static Logger _log = LogManager.GetCurrentClassLogger(); + + + public async Task SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model) + { + return await SendToSonarr(sonarrSettings, model, string.Empty); + } + + + public async Task SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, + string qualityId) + { + var qualityProfile = 0; + if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality + { + int.TryParse(qualityId, out qualityProfile); + } + + if (qualityProfile <= 0) + { + int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); + } + var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetSonarrRootPath(model.RootFolderSelected, sonarrSettings); + + var episodeRequest = model.Episodes.Any(); + var requestAll = model.SeasonsRequested?.Equals("All", StringComparison.CurrentCultureIgnoreCase); + var first = model.SeasonsRequested?.Equals("First", StringComparison.CurrentCultureIgnoreCase); + var latest = model.SeasonsRequested?.Equals("Latest", StringComparison.CurrentCultureIgnoreCase); + var specificSeasonRequest = model.SeasonList?.Any(); + + if (episodeRequest) + { + return await ProcessSonarrEpisodeRequest(sonarrSettings, model, qualityProfile, rootFolderPath); + } + + if (requestAll ?? false) + { + return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); + } + + if (first ?? false) + { + return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); + } + + if (latest ?? false) + { + return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); + } + + if (specificSeasonRequest ?? false) + { + return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); + } + + return null; + } + + + + private async Task ProcessSonarrRequestSeason(SonarrSettings sonarrSettings, RequestedModel model, int qualityId, string rootFolderPath) + { + // Does the series exist? + var series = await GetSonarrSeries(sonarrSettings, model.ProviderId); + if (series == null) + { + //WORKS + // Add the series + return AddSeries(sonarrSettings, model, rootFolderPath, qualityId); + } + + // Also make sure the series is now monitored otherwise we won't search for it + series.monitored = true; + foreach (var seasons in series.seasons) + { + seasons.monitored = true; + } + + // Send the update command + series = SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri); + SonarrApi.SearchForSeries(series.id, sonarrSettings.ApiKey, sonarrSettings.FullUri); + return new SonarrAddSeries { title = series.title }; + } + + + + private async Task ProcessSonarrEpisodeRequest(SonarrSettings sonarrSettings, RequestedModel model, int qualityId, string rootFolderPath) + { + // Does the series exist? + + var series = await GetSonarrSeries(sonarrSettings, model.ProviderId); + if (series == null) + { + var seriesToAdd = new SonarrAddSeries + { + seasonFolder = sonarrSettings.SeasonFolders, + title = model.Title, + qualityProfileId = qualityId, + tvdbId = model.ProviderId, + titleSlug = model.Title, + seasons = new List(), + rootFolderPath = rootFolderPath, + monitored = true, // Montior the series + images = new List(), + addOptions = new AddOptions + { + ignoreEpisodesWithFiles = true, // We don't really care about these + ignoreEpisodesWithoutFiles = true, // We do not want to grab random episodes missing + searchForMissingEpisodes = false // we want don't want to search for the missing episodes either + } + }; + + for (var i = 1; i <= model.SeasonCount; i++) + { + var season = new Season + { + seasonNumber = i, + monitored = false // Do not monitor any seasons + }; + seriesToAdd.seasons.Add(season); + } + + // Add the series now + var result = SonarrApi.AddSeries(seriesToAdd, sonarrSettings.ApiKey, sonarrSettings.FullUri); + + await RequestEpisodesForSonarr(model, result.id, sonarrSettings); + + } + else + { + await RequestEpisodesForSonarr(model, series.id, sonarrSettings); + } + + return new SonarrAddSeries() { title = model.Title }; + } + + public SonarrAddSeries AddSeries(SonarrSettings sonarrSettings, RequestedModel model, string rootFolderPath, int qualityId) + { + //WORKS + // Add the series + var seriesToAdd = new SonarrAddSeries + { + seasonFolder = sonarrSettings.SeasonFolders, + title = model.Title, + qualityProfileId = qualityId, + tvdbId = model.ProviderId, + titleSlug = model.Title, + seasons = new List(), + rootFolderPath = rootFolderPath, + monitored = true, // Montior the series + images = new List(), + addOptions = new AddOptions + { + ignoreEpisodesWithFiles = true, // We don't really care about these + ignoreEpisodesWithoutFiles = false, // We want to get the whole season + searchForMissingEpisodes = true // we want to search for missing + } + }; + + for (var i = 1; i <= model.SeasonCount; i++) + { + var season = new Season + { + seasonNumber = i, + // The model.SeasonList.Lenth is 0 when this is a "request all" + monitored = model.SeasonList.Length == 0 || model.SeasonList.Any(x => x == i) + }; + seriesToAdd.seasons.Add(season); + } + + return SonarrApi.AddSeries(seriesToAdd, sonarrSettings.ApiKey, sonarrSettings.FullUri); + } + + private async Task RequestEpisodesForSonarr(RequestedModel model, int showId, SonarrSettings sonarrSettings) + { + // Now lookup all episodes + var ep = SonarrApi.GetEpisodes(showId.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri); + var episodes = ep?.ToList() ?? new List(); + + var internalEpisodeIds = new List(); + var tasks = new List(); + foreach (var r in model.Episodes) + { + // Match the episode and season number. + // If the episode is monitored we might not be searching for it. + var episode = + episodes.FirstOrDefault( + x => x.episodeNumber == r.EpisodeNumber && x.seasonNumber == r.SeasonNumber); + if (episode == null) + { + continue; + } + var episodeInfo = SonarrApi.GetEpisode(episode.id.ToString(), sonarrSettings.ApiKey, + sonarrSettings.FullUri); + episodeInfo.monitored = true; // Set the episode to monitored + tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey, + sonarrSettings.FullUri))); + internalEpisodeIds.Add(episode.id); + } + + await Task.WhenAll(tasks.ToArray()); + + SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri); + } + + public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) + { + return SendToSickRage(sickRageSettings, model, sickRageSettings.QualityProfile); + } + + public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model, string qualityId) + { + _log.Info("Sending to SickRage {0}", model.Title); + if (sickRageSettings.Qualities.All(x => x.Key != qualityId)) + { + qualityId = sickRageSettings.QualityProfile; + } + + var apiResult = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, qualityId, + sickRageSettings.ApiKey, sickRageSettings.FullUri); + + var result = apiResult.Result; + + + return result; + } + + private async Task GetSonarrSeries(SonarrSettings sonarrSettings, int showId) + { + var task = await Task.Run(() => SonarrApi.GetSeries(sonarrSettings.ApiKey, sonarrSettings.FullUri)).ConfigureAwait(false); + var selectedSeries = task.FirstOrDefault(series => series.tvdbId == showId); + + return selectedSeries; + } + + private async Task GetSonarrRootPath(int pathId, SonarrSettings sonarrSettings) + { + var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () => + { + return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + }); + + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { + return r.path; + } + return string.Empty; + } + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/PlexContentCacher.cs b/Ombi.Services/Jobs/PlexContentCacher.cs index 5b6dc55d4..d4f872fc2 100644 --- a/Ombi.Services/Jobs/PlexContentCacher.cs +++ b/Ombi.Services/Jobs/PlexContentCacher.cs @@ -276,7 +276,8 @@ private List CachedLibraries(PlexSettings plexSettings) Title = m.Title, Type = Store.Models.Plex.PlexMediaType.Movie, Url = m.Url, - ItemId = m.ItemId + ItemId = m.ItemId, + AddedAt = DateTime.UtcNow, }); } } @@ -318,7 +319,8 @@ private List CachedLibraries(PlexSettings plexSettings) Type = Store.Models.Plex.PlexMediaType.Show, Url = t.Url, Seasons = ByteConverterHelper.ReturnBytes(t.Seasons), - ItemId = t.ItemId + ItemId = t.ItemId, + AddedAt = DateTime.UtcNow, }); } } @@ -360,7 +362,8 @@ private List CachedLibraries(PlexSettings plexSettings) Title = a.Title, Type = Store.Models.Plex.PlexMediaType.Artist, Url = a.Url, - ItemId = "album" + ItemId = "album", + AddedAt = DateTime.UtcNow, }); } } diff --git a/Ombi.Services/Jobs/PlexEpisodeCacher.cs b/Ombi.Services/Jobs/PlexEpisodeCacher.cs index 58ebe7fd3..6939ae924 100644 --- a/Ombi.Services/Jobs/PlexEpisodeCacher.cs +++ b/Ombi.Services/Jobs/PlexEpisodeCacher.cs @@ -109,7 +109,7 @@ public void CacheEpisodes(PlexSettings settings) var metadata = PlexApi.GetEpisodeMetaData(settings.PlexAuthToken, settings.FullUri, video.RatingKey); // Loop through the metadata and create the model to insert into the DB - foreach (var metadataVideo in metadata.Video) + foreach (var metadataVideo in metadata?.Video ?? new List