diff --git a/src/Ombi.Api/Api.cs b/src/Ombi.Api/Api.cs index c12258b8e..98fff5e0c 100644 --- a/src/Ombi.Api/Api.cs +++ b/src/Ombi.Api/Api.cs @@ -1,4 +1,7 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -6,6 +9,7 @@ using System.Xml.Serialization; using Newtonsoft.Json; using Microsoft.Extensions.Logging; using Ombi.Helpers; +using Polly; namespace Ombi.Api { @@ -36,6 +40,30 @@ namespace Ombi.Api if (!httpResponseMessage.IsSuccessStatusCode) { LogError(request, httpResponseMessage); + if (request.Retry) + { + + var result = Policy + .Handle() + .OrResult(r => request.StatusCodeToRetry.Contains(r.StatusCode)) + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(10), + }, (exception, timeSpan, context) => + { + + Logger.LogError(LoggingEvents.Api, + $"Retrying RequestUri: {request.FullUri} Because we got Status Code: {exception?.Result?.StatusCode}"); + }); + + httpResponseMessage = await result.ExecuteAsync(async () => + { + using (var req = await httpRequestMessage.Clone()) + { + return await _client.SendAsync(req); + } + }); + } } // do something with the response diff --git a/src/Ombi.Api/HttpRequestExtnesions.cs b/src/Ombi.Api/HttpRequestExtnesions.cs new file mode 100644 index 000000000..fa2ded97d --- /dev/null +++ b/src/Ombi.Api/HttpRequestExtnesions.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api +{ + public static class HttpRequestExtnesions + { + public static async Task Clone(this HttpRequestMessage request) + { + var clone = new HttpRequestMessage(request.Method, request.RequestUri) + { + Content = await request.Content.Clone(), + Version = request.Version + }; + foreach (KeyValuePair prop in request.Properties) + { + clone.Properties.Add(prop); + } + foreach (KeyValuePair> header in request.Headers) + { + clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clone; + } + + public static async Task Clone(this HttpContent content) + { + if (content == null) return null; + + var ms = new MemoryStream(); + await content.CopyToAsync(ms); + ms.Position = 0; + + var clone = new StreamContent(ms); + foreach (KeyValuePair> header in content.Headers) + { + clone.Headers.Add(header.Key, header.Value); + } + return clone; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Api/Ombi.Api.csproj b/src/Ombi.Api/Ombi.Api.csproj index e89eb54b1..325f316b8 100644 --- a/src/Ombi.Api/Ombi.Api.csproj +++ b/src/Ombi.Api/Ombi.Api.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Ombi.Api/Request.cs b/src/Ombi.Api/Request.cs index 16dd38055..e4120ed9c 100644 --- a/src/Ombi.Api/Request.cs +++ b/src/Ombi.Api/Request.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Text; @@ -25,6 +26,9 @@ namespace Ombi.Api public string BaseUrl { get; } public HttpMethod HttpMethod { get; } + public bool Retry { get; set; } + public List StatusCodeToRetry { get; set; } = new List(); + public Action OnBeforeDeserialization { get; set; } private string FullUrl diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 817dfd551..b40d49036 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -172,6 +172,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/Ombi.Schedule/JobSetup.cs b/src/Ombi.Schedule/JobSetup.cs index dc9d49269..e73302387 100644 --- a/src/Ombi.Schedule/JobSetup.cs +++ b/src/Ombi.Schedule/JobSetup.cs @@ -17,46 +17,49 @@ namespace Ombi.Schedule public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync, IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, - ISettingsService jobsettings, ISickRageSync srSync) + ISettingsService jobsettings, ISickRageSync srSync, IRefreshMetadata refresh) { - PlexContentSync = plexContentSync; - RadarrSync = radarrSync; - Updater = updater; - EmbyContentSync = embySync; - PlexUserImporter = userImporter; - EmbyUserImporter = embyUserImporter; - SonarrSync = cache; - CpCache = cpCache; - JobSettings = jobsettings; - SrSync = srSync; + _plexContentSync = plexContentSync; + _radarrSync = radarrSync; + _updater = updater; + _embyContentSync = embySync; + _plexUserImporter = userImporter; + _embyUserImporter = embyUserImporter; + _sonarrSync = cache; + _cpCache = cpCache; + _jobSettings = jobsettings; + _srSync = srSync; + _refreshMetadata = refresh; } - private IPlexContentSync PlexContentSync { get; } - private IRadarrSync RadarrSync { get; } - private IOmbiAutomaticUpdater Updater { get; } - private IPlexUserImporter PlexUserImporter { get; } - private IEmbyContentSync EmbyContentSync { get; } - private IEmbyUserImporter EmbyUserImporter { get; } - private ISonarrSync SonarrSync { get; } - private ICouchPotatoSync CpCache { get; } - private ISickRageSync SrSync { get; } - private ISettingsService JobSettings { get; set; } + private readonly IPlexContentSync _plexContentSync; + private readonly IRadarrSync _radarrSync; + private readonly IOmbiAutomaticUpdater _updater; + private readonly IPlexUserImporter _plexUserImporter; + private readonly IEmbyContentSync _embyContentSync; + private readonly IEmbyUserImporter _embyUserImporter; + private readonly ISonarrSync _sonarrSync; + private readonly ICouchPotatoSync _cpCache; + private readonly ISickRageSync _srSync; + private readonly ISettingsService _jobSettings; + private readonly IRefreshMetadata _refreshMetadata; public void Setup() { - var s = JobSettings.GetSettings(); + var s = _jobSettings.GetSettings(); - RecurringJob.AddOrUpdate(() => EmbyContentSync.Start(), JobSettingsHelper.EmbyContent(s)); - RecurringJob.AddOrUpdate(() => SonarrSync.Start(), JobSettingsHelper.Sonarr(s)); - RecurringJob.AddOrUpdate(() => RadarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); - RecurringJob.AddOrUpdate(() => PlexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s)); - RecurringJob.AddOrUpdate(() => CpCache.Start(), JobSettingsHelper.CouchPotato(s)); - RecurringJob.AddOrUpdate(() => SrSync.Start(), JobSettingsHelper.SickRageSync(s)); + RecurringJob.AddOrUpdate(() => _embyContentSync.Start(), JobSettingsHelper.EmbyContent(s)); + RecurringJob.AddOrUpdate(() => _sonarrSync.Start(), JobSettingsHelper.Sonarr(s)); + RecurringJob.AddOrUpdate(() => _radarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); + RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s)); + RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s)); + RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s)); + RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s)); - RecurringJob.AddOrUpdate(() => Updater.Update(null), JobSettingsHelper.Updater(s)); + RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s)); - RecurringJob.AddOrUpdate(() => EmbyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); - RecurringJob.AddOrUpdate(() => PlexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); + RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); + RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); } } } diff --git a/src/Ombi.Schedule/Jobs/Ombi/IRefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/IRefreshMetadata.cs new file mode 100644 index 000000000..a08db74d0 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/IRefreshMetadata.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public interface IRefreshMetadata : IBaseJob + { + Task Start(); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs new file mode 100644 index 000000000..5f5dd4635 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -0,0 +1,249 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Api.TvMaze; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public class RefreshMetadata : IRefreshMetadata + { + public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, + ILogger log, ITvMazeApi tvApi, ISettingsService plexSettings, + IMovieDbApi movieApi) + { + _plexRepo = plexRepo; + _embyRepo = embyRepo; + _log = log; + _movieApi = movieApi; + _tvApi = tvApi; + _plexSettings = plexSettings; + } + + private readonly IPlexContentRepository _plexRepo; + private readonly IEmbyContentRepository _embyRepo; + private readonly ILogger _log; + private readonly IMovieDbApi _movieApi; + private readonly ITvMazeApi _tvApi; + private readonly ISettingsService _plexSettings; + + public async Task Start() + { + _log.LogInformation("Starting the Metadata refresh"); + try + { + var settings = await _plexSettings.GetSettingsAsync(); + if (settings.Enable) + { + await StartPlex(); + } + } + catch (Exception e) + { + _log.LogError(e, "Exception when refreshing the Plex Metadata"); + throw; + } + } + + private async Task StartPlex() + { + await StartPlexMovies(); + + // Now Tv + await StartPlexTv(); + } + + private async Task StartPlexTv() + { + var allTv = _plexRepo.GetAll().Where(x => + x.Type == PlexMediaTypeEntity.Show && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue())); + var tvCount = 0; + foreach (var show in allTv) + { + var hasImdb = show.ImdbId.HasValue(); + var hasTheMovieDb = show.TheMovieDbId.HasValue(); + var hasTvDbId = show.TvDbId.HasValue(); + + if (!hasTheMovieDb) + { + var id = await GetTheMovieDbId(hasTvDbId, hasImdb, show.TvDbId, show.ImdbId, show.Title); + show.TheMovieDbId = id; + } + + if (!hasImdb) + { + var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId); + show.ImdbId = id; + _plexRepo.UpdateWithoutSave(show); + } + + if (!hasTvDbId) + { + var id = await GetTvDbId(hasTheMovieDb, hasImdb, show.TheMovieDbId, show.ImdbId, show.Title); + show.TvDbId = id; + _plexRepo.UpdateWithoutSave(show); + } + tvCount++; + if (tvCount >= 20) + { + await _plexRepo.SaveChangesAsync(); + tvCount = 0; + } + } + await _plexRepo.SaveChangesAsync(); + } + + private async Task StartPlexMovies() + { + var allMovies = _plexRepo.GetAll().Where(x => + x.Type == PlexMediaTypeEntity.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())); + int movieCount = 0; + foreach (var movie in allMovies) + { + var hasImdb = movie.ImdbId.HasValue(); + var hasTheMovieDb = movie.TheMovieDbId.HasValue(); + // Movies don't really use TheTvDb + + if (!hasImdb) + { + var imdbId = await GetImdbId(hasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty); + movie.ImdbId = imdbId; + _plexRepo.UpdateWithoutSave(movie); + } + if (!hasTheMovieDb) + { + var id = await GetTheMovieDbId(false, hasImdb, string.Empty, movie.ImdbId, movie.Title); + movie.TheMovieDbId = id; + _plexRepo.UpdateWithoutSave(movie); + } + movieCount++; + if (movieCount >= 20) + { + await _plexRepo.SaveChangesAsync(); + movieCount = 0; + } + } + + await _plexRepo.SaveChangesAsync(); + } + + private async Task GetTheMovieDbId(bool hasTvDbId, bool hasImdb, string tvdbID, string imdbId, string title) + { + _log.LogInformation("The Media item {0} does not have a TheMovieDbId, searching for TheMovieDbId", title); + FindResult result = null; + var hasResult = false; + if (hasTvDbId) + { + result = await _movieApi.Find(tvdbID, ExternalSource.tvdb_id); + hasResult = result?.tv_results?.Length > 0; + + _log.LogInformation("Setting Show {0} because we have TvDbId, result: {1}", title, hasResult); + } + if (hasImdb && !hasResult) + { + result = await _movieApi.Find(imdbId, ExternalSource.imdb_id); + hasResult = result?.tv_results?.Length > 0; + + _log.LogInformation("Setting Show {0} because we have ImdbId, result: {1}", title, hasResult); + } + if (hasResult) + { + return result.tv_results?[0]?.id.ToString() ?? string.Empty; + } + return string.Empty; + } + + private async Task GetImdbId(bool hasTheMovieDb, bool hasTvDbId, string title, string theMovieDbId, string tvDbId) + { + _log.LogInformation("The media item {0} does not have a ImdbId, searching for ImdbId", title); + // Looks like TV Maze does not provide the moviedb id, neither does the TV endpoint on TheMovieDb + if (hasTheMovieDb) + { + _log.LogInformation("The show {0} has TheMovieDbId but not ImdbId, searching for ImdbId", title); + if (int.TryParse(theMovieDbId, out var id)) + { + var result = await _movieApi.GetTvExternals(id); + + return result.imdb_id; + } + } + + if (hasTvDbId) + { + _log.LogInformation("The show {0} has tvdbid but not ImdbId, searching for ImdbId", title); + if (int.TryParse(tvDbId, out var id)) + { + var result = await _tvApi.ShowLookupByTheTvDbId(id); + return result?.externals?.imdb; + } + } + return string.Empty; + } + + + private async Task GetTvDbId(bool hasTheMovieDb, bool hasImdb, string theMovieDbId, string imdbId, string title) + { + _log.LogInformation("The media item {0} does not have a TvDbId, searching for TvDbId", title); + if (hasTheMovieDb) + { + _log.LogInformation("The show {0} has theMovieDBId but not ImdbId, searching for ImdbId", title); + if (int.TryParse(theMovieDbId, out var id)) + { + var result = await _movieApi.GetTvExternals(id); + + return result.tvdb_id.ToString(); + } + } + + if (hasImdb) + { + _log.LogInformation("The show {0} has ImdbId but not ImdbId, searching for ImdbId", title); + var result = await _movieApi.Find(imdbId, ExternalSource.imdb_id); + if (result?.tv_results?.Length > 0) + { + var movieId = result.tv_results?[0]?.id ?? 0; + + var externalResult = await _movieApi.GetTvExternals(movieId); + + return externalResult.imdb_id; + } + } + return string.Empty; + } + + + private async Task StartEmby() + { + + } + + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _plexRepo?.Dispose(); + _embyRepo?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Ombi.Schedule.csproj b/src/Ombi.Schedule/Ombi.Schedule.csproj index cb8cef8ab..5088bc9f8 100644 --- a/src/Ombi.Schedule/Ombi.Schedule.csproj +++ b/src/Ombi.Schedule/Ombi.Schedule.csproj @@ -32,8 +32,10 @@ + + \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/JobSettings.cs b/src/Ombi.Settings/Settings/Models/JobSettings.cs index 7cf6e7104..ef4335fa5 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettings.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettings.cs @@ -10,5 +10,6 @@ public string AutomaticUpdater { get; set; } public string UserImporter { get; set; } public string SickRageSync { get; set; } + public string RefreshMetadata { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs index 69eaf4b33..a200c6b49 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs @@ -39,6 +39,10 @@ namespace Ombi.Settings.Settings.Models { return Get(s.SickRageSync, Cron.Hourly(35)); } + public static string RefreshMetadata(JobSettings s) + { + return Get(s.RefreshMetadata, Cron.Daily(3)); + } private static string Get(string settings, string defaultCron) diff --git a/src/Ombi.Store/Repository/IPlexContentRepository.cs b/src/Ombi.Store/Repository/IPlexContentRepository.cs index 2fef89be2..381a89fa3 100644 --- a/src/Ombi.Store/Repository/IPlexContentRepository.cs +++ b/src/Ombi.Store/Repository/IPlexContentRepository.cs @@ -22,5 +22,7 @@ namespace Ombi.Store.Repository Task DeleteEpisode(PlexEpisode content); void DeleteWithoutSave(PlexServerContent content); void DeleteWithoutSave(PlexEpisode content); + Task UpdateRange(IEnumerable existingContent); + void UpdateWithoutSave(PlexServerContent existingContent); } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/IRepository.cs b/src/Ombi.Store/Repository/IRepository.cs index ed5ed28c5..c85b45d8f 100644 --- a/src/Ombi.Store/Repository/IRepository.cs +++ b/src/Ombi.Store/Repository/IRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Ombi.Store.Entities; @@ -24,5 +25,6 @@ namespace Ombi.Store.Repository where TEntity : class; Task ExecuteSql(string sql); + DbSet _db { get; } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/PlexContentRepository.cs b/src/Ombi.Store/Repository/PlexContentRepository.cs index 56fec441a..098466310 100644 --- a/src/Ombi.Store/Repository/PlexContentRepository.cs +++ b/src/Ombi.Store/Repository/PlexContentRepository.cs @@ -97,6 +97,16 @@ namespace Ombi.Store.Repository Db.PlexServerContent.Update(existingContent); await Db.SaveChangesAsync(); } + public void UpdateWithoutSave(PlexServerContent existingContent) + { + Db.PlexServerContent.Update(existingContent); + } + + public async Task UpdateRange(IEnumerable existingContent) + { + Db.PlexServerContent.UpdateRange(existingContent); + await Db.SaveChangesAsync(); + } public IQueryable GetAllEpisodes() { diff --git a/src/Ombi.Store/Repository/Repository.cs b/src/Ombi.Store/Repository/Repository.cs index b4b9f8e93..049da0356 100644 --- a/src/Ombi.Store/Repository/Repository.cs +++ b/src/Ombi.Store/Repository/Repository.cs @@ -17,7 +17,7 @@ namespace Ombi.Store.Repository _ctx = ctx; _db = _ctx.Set(); } - private readonly DbSet _db; + public DbSet _db { get; } private readonly IOmbiContext _ctx; public async Task Find(object key) diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index dd0d0e92c..787902a4b 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -15,5 +15,7 @@ namespace Ombi.Api.TheMovieDb Task> TopRated(); Task> Upcoming(); Task> SimilarMovies(int movieId); + Task Find(string externalId, ExternalSource source); + Task GetTvExternals(int theMovieDbId); } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/FindResult.cs b/src/Ombi.TheMovieDbApi/Models/FindResult.cs new file mode 100644 index 000000000..f76fca564 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/FindResult.cs @@ -0,0 +1,52 @@ +namespace Ombi.Api.TheMovieDb.Models +{ + public class FindResult + { + public Movie_Results[] movie_results { get; set; } + public object[] person_results { get; set; } + public TvResults[] tv_results { get; set; } + public object[] tv_episode_results { get; set; } + public object[] tv_season_results { get; set; } + } + + public class Movie_Results + { + public bool adult { get; set; } + public string backdrop_path { get; set; } + public int[] genre_ids { get; set; } + public int id { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public string overview { get; set; } + public string poster_path { get; set; } + public string release_date { get; set; } + public string title { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public int vote_count { get; set; } + } + + + public class TvResults + { + public string original_name { get; set; } + public int id { get; set; } + public string name { get; set; } + public int vote_count { get; set; } + public float vote_average { get; set; } + public string first_air_date { get; set; } + public string poster_path { get; set; } + public int[] genre_ids { get; set; } + public string original_language { get; set; } + public string backdrop_path { get; set; } + public string overview { get; set; } + public string[] origin_country { get; set; } + } + + + public enum ExternalSource + { + imdb_id, + tvdb_id + } +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/TvExternals.cs b/src/Ombi.TheMovieDbApi/Models/TvExternals.cs new file mode 100644 index 000000000..237ae36a7 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/TvExternals.cs @@ -0,0 +1,16 @@ +namespace Ombi.Api.TheMovieDb.Models +{ + public class TvExternals + { + public string imdb_id { get; set; } + public string freebase_mid { get; set; } + public string freebase_id { get; set; } + public int tvdb_id { get; set; } + public int tvrage_id { get; set; } + public string facebook_id { get; set; } + public object instagram_id { get; set; } + public object twitter_id { get; set; } + public int id { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 1fbfe9aaf..08925e490 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using AutoMapper; @@ -25,15 +26,37 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request(request); return Mapper.Map(result); } + public async Task Find(string externalId, ExternalSource source) + { + var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); + + request.AddQueryString("external_source", source.ToString()); + + return await Api.Request(request); + } + + public async Task GetTvExternals(int theMovieDbId) + { + var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); + + return await Api.Request(request); + } + public async Task> SimilarMovies(int movieId) { var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -44,6 +67,7 @@ namespace Ombi.Api.TheMovieDb var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates"); + AddRetry(request); var result = await Api.Request(request); return Mapper.Map(result); } @@ -53,6 +77,7 @@ namespace Ombi.Api.TheMovieDb var request = new Request($"search/movie", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -62,6 +87,7 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/popular", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -70,6 +96,7 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -78,6 +105,7 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -86,9 +114,14 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - + private static void AddRetry(Request request) + { + request.Retry = true; + request.StatusCodeToRetry.Add((HttpStatusCode)429); + } } } diff --git a/src/Ombi/ClientApp/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/app/interfaces/ISettings.ts index 1ba2c4843..c79860801 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISettings.ts @@ -125,6 +125,7 @@ export interface IJobSettings { automaticUpdater: string; userImporter: string; sickRageSync: string; + refreshMetadata: string; } export interface IIssueSettings extends ISettings { @@ -193,3 +194,18 @@ export interface IDogNzbSettings extends ISettings { export interface IIssueCategory extends ISettings { value: string; } + +export interface ICronTestModel { + success: boolean; + message: string; + schedule: Date[]; +} + +export interface ICronViewModelBody { + expression: string; +} + +export interface IJobSettingsViewModel { + result: boolean; + message: string; +} diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index 059df61e8..54f3458d5 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -7,6 +7,8 @@ import { IAbout, IAuthenticationSettings, ICouchPotatoSettings, + ICronTestModel, + ICronViewModelBody, ICustomizationSettings, IDiscordNotifcationSettings, IDogNzbSettings, @@ -14,6 +16,7 @@ import { IEmbySettings, IIssueSettings, IJobSettings, + IJobSettingsViewModel, ILandingPageSettings, IMattermostNotifcationSettings, IMobileNotifcationSettings, @@ -231,10 +234,15 @@ export class SettingsService extends ServiceHelpers { return this.http.get(`${this.url}/jobs`, {headers: this.headers}); } - public saveJobSettings(settings: IJobSettings): Observable { + public saveJobSettings(settings: IJobSettings): Observable { return this.http - .post(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers}); - } + .post(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers}); + } + + public testCron(body: ICronViewModelBody): Observable { + return this.http + .post(`${this.url}/testcron`, JSON.stringify(body), {headers: this.headers}); + } public getSickRageSettings(): Observable { return this.http.get(`${this.url}/sickrage`, {headers: this.headers}); diff --git a/src/Ombi/ClientApp/app/settings/jobs/jobs.component.html b/src/Ombi/ClientApp/app/settings/jobs/jobs.component.html index ff5e56ed8..02eb51d77 100644 --- a/src/Ombi/ClientApp/app/settings/jobs/jobs.component.html +++ b/src/Ombi/ClientApp/app/settings/jobs/jobs.component.html @@ -12,29 +12,34 @@ The Sonarr Sync is required +
The SickRage Sync is required +
The Radarr Sync is required +
The CouchPotato Sync is required +
The Automatic Update is required +
@@ -50,21 +55,37 @@ The Plex Sync is required +
The Emby Sync is required +
The User Importer is required + +
+ +
+ + + The Refresh Metadata is required +
- \ No newline at end of file + + + +
    +
  • {{item | date:'short'}}
  • +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/jobs/jobs.component.ts b/src/Ombi/ClientApp/app/settings/jobs/jobs.component.ts index f547c5056..380cef8de 100644 --- a/src/Ombi/ClientApp/app/settings/jobs/jobs.component.ts +++ b/src/Ombi/ClientApp/app/settings/jobs/jobs.component.ts @@ -1,7 +1,10 @@ import { Component, OnInit } from "@angular/core"; + import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { NotificationService, SettingsService } from "../../services"; +import { ICronTestModel } from "./../../interfaces"; + @Component({ templateUrl: "./jobs.component.html", }) @@ -10,6 +13,8 @@ export class JobsComponent implements OnInit { public form: FormGroup; public profilesRunning: boolean; + public testModel: ICronTestModel; + public displayTest: boolean; constructor(private readonly settingsService: SettingsService, private readonly fb: FormBuilder, @@ -26,7 +31,19 @@ export class JobsComponent implements OnInit { sonarrSync: [x.radarrSync, Validators.required], radarrSync: [x.sonarrSync, Validators.required], sickRageSync: [x.sickRageSync, Validators.required], - }); + refreshMetadata: [x.refreshMetadata, Validators.required], + }); + }); + } + + public testCron(expression: string) { + this.settingsService.testCron({ expression }).subscribe(x => { + if(x.success) { + this.testModel = x; + this.displayTest = true; + } else { + this.notificationService.error(x.message); + } }); } @@ -37,10 +54,10 @@ export class JobsComponent implements OnInit { } const settings = form.value; this.settingsService.saveJobSettings(settings).subscribe(x => { - if (x) { + if (x.result) { this.notificationService.success("Successfully saved the job settings"); } else { - this.notificationService.success("There was an error when saving the job settings"); + this.notificationService.error("There was an error when saving the job settings. " + x.message); } }); } diff --git a/src/Ombi/ClientApp/app/settings/settings.module.ts b/src/Ombi/ClientApp/app/settings/settings.module.ts index f10df8448..c95aa4362 100644 --- a/src/Ombi/ClientApp/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/app/settings/settings.module.ts @@ -41,7 +41,7 @@ import { WikiComponent } from "./wiki.component"; import { SettingsMenuComponent } from "./settingsmenu.component"; -import { AutoCompleteModule, CalendarModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng"; +import { AutoCompleteModule, CalendarModule, DialogModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng"; const routes: Routes = [ { path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] }, @@ -88,6 +88,7 @@ const routes: Routes = [ ClipboardModule, PipeModule, RadioButtonModule, + DialogModule, ], declarations: [ SettingsMenuComponent, @@ -139,4 +140,4 @@ const routes: Routes = [ ], }) -export class SettingsModule { } +export class SettingsModule { } \ No newline at end of file diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index c68a648c8..c0148c1bb 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -5,15 +5,18 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using AutoMapper; using Hangfire; +using Hangfire.RecurringJobExtensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.PlatformAbstractions; +using NCrontab; using Ombi.Api.Emby; using Ombi.Attributes; using Ombi.Core.Models.UI; @@ -465,7 +468,8 @@ namespace Ombi.Controllers j.PlexContentSync = j.PlexContentSync.HasValue() ? j.PlexContentSync : JobSettingsHelper.PlexContent(j); j.UserImporter = j.UserImporter.HasValue() ? j.UserImporter : JobSettingsHelper.UserImporter(j); j.SickRageSync = j.SickRageSync.HasValue() ? j.SickRageSync : JobSettingsHelper.SickRageSync(j); - + j.RefreshMetadata = j.RefreshMetadata.HasValue() ? j.RefreshMetadata : JobSettingsHelper.RefreshMetadata(j); + return j; } @@ -475,9 +479,71 @@ namespace Ombi.Controllers /// The settings. /// [HttpPost("jobs")] - public async Task JobSettings([FromBody]JobSettings settings) + public async Task JobSettings([FromBody]JobSettings settings) { - return await Save(settings); + // Verify that we have correct CRON's + foreach (var propertyInfo in settings.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name.Equals("Id", StringComparison.CurrentCultureIgnoreCase)) + { + continue; + } + var expression = (string)propertyInfo.GetValue(settings, null); + + try + { + var r = CrontabSchedule.TryParse(expression); + if (r == null) + { + return new JobSettingsViewModel + { + Message = $"{propertyInfo.Name} does not have a valid CRON Expression" + }; + } + } + catch (Exception) + { + return new JobSettingsViewModel + { + Message = $"{propertyInfo.Name} does not have a valid CRON Expression" + }; + } + } + var result = await Save(settings); + + return new JobSettingsViewModel + { + Result = result + }; + } + + [HttpPost("testcron")] + public CronTestModel TestCron([FromBody] CronViewModelBody body) + { + var model = new CronTestModel(); + try + { + var time = DateTime.UtcNow; + var result = CrontabSchedule.TryParse(body.Expression); + for (int i = 0; i < 10; i++) + { + var next = result.GetNextOccurrence(time); + model.Schedule.Add(next); + time = next; + } + model.Success = true; + return model; + } + catch (Exception) + { + return new CronTestModel + { + Message = $"CRON Expression {body.Expression} is not valid" + }; + } + + } diff --git a/src/Ombi/Models/CronTestModel.cs b/src/Ombi/Models/CronTestModel.cs new file mode 100644 index 000000000..9698afbff --- /dev/null +++ b/src/Ombi/Models/CronTestModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Ombi.Models +{ + public class CronTestModel + { + public bool Success { get; set; } + public string Message { get; set; } + public List Schedule { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Ombi/Models/CronViewModelBody.cs b/src/Ombi/Models/CronViewModelBody.cs new file mode 100644 index 000000000..cd961eda1 --- /dev/null +++ b/src/Ombi/Models/CronViewModelBody.cs @@ -0,0 +1,7 @@ +namespace Ombi.Models +{ + public class CronViewModelBody + { + public string Expression { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi/Models/JobSettingsViewModel.cs b/src/Ombi/Models/JobSettingsViewModel.cs new file mode 100644 index 000000000..75006b86a --- /dev/null +++ b/src/Ombi/Models/JobSettingsViewModel.cs @@ -0,0 +1,8 @@ +namespace Ombi.Models +{ + public class JobSettingsViewModel + { + public bool Result { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 0c2c73360..5a465fb76 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -66,6 +66,7 @@ +