From 9886c404992353191b58dfe9edfaedf65f5c2ee0 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Fri, 20 Jan 2017 15:56:49 +0000 Subject: [PATCH] Added a bunch of categories for tv search similar to what we have for movies. --- Ombi.Api.Interfaces/ITraktApi.cs | 16 + .../Ombi.Api.Interfaces.csproj | 9 + Ombi.Api.Interfaces/packages.config | 2 + Ombi.Api/Ombi.Api.csproj | 5 + Ombi.Api/TraktApi.cs | 51 ++ Ombi.Api/packages.config | 1 + Ombi.Helpers/DateTimeHelper.cs | 7 + Ombi.UI/Content/search.js | 59 ++- Ombi.UI/Models/SearchTvShowViewModel.cs | 15 + Ombi.UI/Modules/SearchModule.cs | 211 +++++++- Ombi.UI/NinjectModules/ApiModule.cs | 1 + Ombi.UI/Ombi.UI.csproj | 4 + Ombi.UI/Views/Search/Index.cshtml | 474 +++++++++--------- Ombi.UI/packages.config | 1 + 14 files changed, 624 insertions(+), 232 deletions(-) create mode 100644 Ombi.Api.Interfaces/ITraktApi.cs create mode 100644 Ombi.Api/TraktApi.cs diff --git a/Ombi.Api.Interfaces/ITraktApi.cs b/Ombi.Api.Interfaces/ITraktApi.cs new file mode 100644 index 000000000..ed20f039c --- /dev/null +++ b/Ombi.Api.Interfaces/ITraktApi.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TraktApiSharp.Enums; +using TraktApiSharp.Objects.Get.Shows; +using TraktApiSharp.Objects.Get.Shows.Common; + +namespace Ombi.Api.Interfaces +{ + public interface ITraktApi + { + Task> GetAnticipatedShows(int? page = default(int?), int? limitPerPage = default(int?)); + Task> GetMostWatchesShows(TraktTimePeriod period = null, int? page = default(int?), int? limitPerPage = default(int?)); + Task> GetPopularShows(int? page = default(int?), int? limitPerPage = default(int?)); + Task> GetTrendingShows(int? page = default(int?), int? limitPerPage = default(int?)); + } +} \ No newline at end of file diff --git a/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj b/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj index 1cc18964a..c8c1ca938 100644 --- a/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj +++ b/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj @@ -31,6 +31,10 @@ 4 + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll True @@ -43,6 +47,10 @@ + + ..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll + True + @@ -58,6 +66,7 @@ + diff --git a/Ombi.Api.Interfaces/packages.config b/Ombi.Api.Interfaces/packages.config index a63cb4deb..5ac87cab2 100644 --- a/Ombi.Api.Interfaces/packages.config +++ b/Ombi.Api.Interfaces/packages.config @@ -1,4 +1,6 @@  + + \ No newline at end of file diff --git a/Ombi.Api/Ombi.Api.csproj b/Ombi.Api/Ombi.Api.csproj index 27a889451..b9e674cc2 100644 --- a/Ombi.Api/Ombi.Api.csproj +++ b/Ombi.Api/Ombi.Api.csproj @@ -66,12 +66,17 @@ ..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll True + + ..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll + True + + diff --git a/Ombi.Api/TraktApi.cs b/Ombi.Api/TraktApi.cs new file mode 100644 index 000000000..ae2ff11b2 --- /dev/null +++ b/Ombi.Api/TraktApi.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ombi.Api.Interfaces; +using Ombi.Helpers; +using TraktApiSharp; +using TraktApiSharp.Enums; +using TraktApiSharp.Objects.Get.Shows; +using TraktApiSharp.Objects.Get.Shows.Common; +using TraktApiSharp.Requests.Params; + +namespace Ombi.Api +{ + public class TraktApi : ITraktApi + { + private TraktClient Client { get; } + + private static readonly string Encrypted = "z/56wM/oEkkCWEvSIZCrzQyUvvqmafQ3njqf0UNK5xuKbNYh5Wz8ocoG2QDa5y1DBkozLaKsGxORmAB1XUvwbnom8DVNo9gE++9GTuwxmGlLDD318PXpRmYmpKqNwFSKRZgF6ewiY9qR4t3iG0pGQwPA08FK3+H7kpOKAGJNR9RMDP9wwB6Vl4DuOiZb9/DETjzZ+/zId0ZqimrbN+PLrg=="; + private readonly string _apiKey = StringCipher.Decrypt(Encrypted, "ApiKey"); + public TraktApi() + { + Client = new TraktClient(_apiKey); + } + + public async Task> GetPopularShows(int? page = null, int? limitPerPage = null) + { + var popular = await Client.Shows.GetPopularShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return popular.Items; + } + + public async Task> GetTrendingShows(int? page = null, int? limitPerPage = null) + { + var trendingShowsTop10 = await Client.Shows.GetTrendingShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return trendingShowsTop10.Items; + } + + public async Task> GetAnticipatedShows(int? page = null, int? limitPerPage = null) + { + var anticipatedShows = await Client.Shows.GetMostAnticipatedShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return anticipatedShows.Items; + } + + public async Task> GetMostWatchesShows(TraktTimePeriod period = null, int? page = null, int? limitPerPage = null) + { + var anticipatedShows = await Client.Shows.GetMostWatchedShowsAsync(period ?? TraktTimePeriod.Monthly, new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return anticipatedShows.Items; + } + } +} diff --git a/Ombi.Api/packages.config b/Ombi.Api/packages.config index ce5ccf24e..a20220585 100644 --- a/Ombi.Api/packages.config +++ b/Ombi.Api/packages.config @@ -8,4 +8,5 @@ + \ No newline at end of file diff --git a/Ombi.Helpers/DateTimeHelper.cs b/Ombi.Helpers/DateTimeHelper.cs index d8dbc681a..b25972ddf 100644 --- a/Ombi.Helpers/DateTimeHelper.cs +++ b/Ombi.Helpers/DateTimeHelper.cs @@ -46,5 +46,12 @@ namespace Ombi.Helpers dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime(); return dtDateTime; } + + public static long ToJavascriptTimestamp(this DateTime input) + { + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var time = input.Subtract(new TimeSpan(epoch.Ticks)); + return (long)(time.Ticks / 10000); + } } } diff --git a/Ombi.UI/Content/search.js b/Ombi.UI/Content/search.js index 8b9c10109..14f675562 100644 --- a/Ombi.UI/Content/search.js +++ b/Ombi.UI/Content/search.js @@ -72,6 +72,25 @@ $(function () { moviesInTheaters(); }); + // TV DropDown + $('#popularShows').on('click', function (e) { + e.preventDefault(); + popularShows(); + }); + + $('#trendingShows').on('click', function (e) { + e.preventDefault(); + trendingTv(); + }); + $('#mostWatchedShows').on('click', function (e) { + e.preventDefault(); + mostwatchedTv(); + }); + $('#anticipatedShows').on('click', function (e) { + e.preventDefault(); + anticipatedTv(); + }); + // Type in TV search $("#tvSearchContent").on("input", function () { if (searchTimer) { @@ -293,6 +312,23 @@ $(function () { getMovies(url); } + function popularShows() { + var url = createBaseUrl(base, '/search/tv/popular'); + getTvShows(url, true); + } + function anticipatedTv() { + var url = createBaseUrl(base, '/search/tv/anticipated'); + getTvShows(url, true); + } + function trendingTv() { + var url = createBaseUrl(base, '/search/tv/trending'); + getTvShows(url, true); + } + function mostwatchedTv() { + var url = createBaseUrl(base, '/search/tv/mostwatched'); + getTvShows(url, true); + } + function getMovies(url) { resetMovies(); @@ -323,10 +359,10 @@ $(function () { var query = $("#tvSearchContent").val(); var url = createBaseUrl(base, '/search/tv/'); - query ? getTvShows(url + query) : resetTvShows(); + query ? getTvShows(url + query, false) : resetTvShows(); } - function getTvShows(url) { + function getTvShows(url, loadImage) { resetTvShows(); $('#tvSearchButton').attr("class", "fa fa-spinner fa-spin"); @@ -338,7 +374,9 @@ $(function () { $("#tvList").append(html); checkNetflix(context.title, context.id); - + if (loadImage) { + getTvPoster(result.id); + } }); } else { @@ -406,6 +444,16 @@ $(function () { }); }; + function getTvPoster(theTvDbId) { + + var url = createBaseUrl(base, '/search/tv/poster/'); + $.ajax(url + theTvDbId).success(function (result) { + if (result) { + $('#' + theTvDbId + "imgDiv").html(" poster"); + } + }); + }; + function buildMovieContext(result) { var date = new Date(result.releaseDate); var year = date.getFullYear(); @@ -432,6 +480,7 @@ $(function () { var date = new Date(result.firstAired); var year = date.getFullYear(); var context = { + status : result.status, posterPath: result.banner, id: result.id, title: result.seriesName, @@ -448,7 +497,9 @@ $(function () { tvPartialAvailable: result.tvPartialAvailable, disableTvRequestsByEpisode: result.disableTvRequestsByEpisode, disableTvRequestsBySeason: result.disableTvRequestsBySeason, - enableTvRequestsForOnlySeries: result.enableTvRequestsForOnlySeries + enableTvRequestsForOnlySeries: result.enableTvRequestsForOnlySeries, + trailer: result.trailer, + homepage: result.homepage }; return context; diff --git a/Ombi.UI/Models/SearchTvShowViewModel.cs b/Ombi.UI/Models/SearchTvShowViewModel.cs index faa6333ba..29380a835 100644 --- a/Ombi.UI/Models/SearchTvShowViewModel.cs +++ b/Ombi.UI/Models/SearchTvShowViewModel.cs @@ -58,5 +58,20 @@ namespace Ombi.UI.Models public bool DisableTvRequestsByEpisode { get; set; } public bool DisableTvRequestsBySeason { get; set; } public bool EnableTvRequestsForOnlySeries { get; set; } + + /// + /// This is used from the Trakt API + /// + /// + /// The trailer. + /// + public string Trailer { get; set; } + /// + /// This is used from the Trakt API + /// + /// + /// The trailer. + /// + public string Homepage { get; set; } } } \ No newline at end of file diff --git a/Ombi.UI/Modules/SearchModule.cs b/Ombi.UI/Modules/SearchModule.cs index 10be9bfdb..63cc4c03c 100644 --- a/Ombi.UI/Modules/SearchModule.cs +++ b/Ombi.UI/Modules/SearchModule.cs @@ -58,6 +58,7 @@ using Ombi.Store.Repository; using Ombi.UI.Helpers; using Ombi.UI.Models; using TMDbLib.Objects.General; +using TraktApiSharp.Objects.Get.Shows; using Action = Ombi.Helpers.Analytics.Action; using EpisodesModel = Ombi.Store.EpisodesModel; using ISecurityExtensions = Ombi.Core.ISecurityExtensions; @@ -76,7 +77,7 @@ namespace Ombi.UI.Modules ISettingsService plexService, ISettingsService auth, IRepository u, ISettingsService email, IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, - ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher) + ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi) : base("search", prSettings, security) { Auth = auth; @@ -109,6 +110,7 @@ namespace Ombi.UI.Modules MovieSender = movieSender; WatcherCacher = watcherCacher; RadarrCacher = radarrCacher; + TraktApi = traktApi; Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); @@ -120,6 +122,13 @@ namespace Ombi.UI.Modules Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); + Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular); + Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending); + Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched); + Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated); + + Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id); + Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); Post["request/tv", true] = async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); @@ -129,6 +138,7 @@ namespace Ombi.UI.Modules Get["/seasons"] = x => GetSeasons(); Get["/episodes", true] = async (x, ct) => await GetEpisodes(); } + private ITraktApi TraktApi { get; } private IWatcherCacher WatcherCacher { get; } private IMovieSender MovieSender { get; } private IRepository PlexContentRepository { get; } @@ -190,6 +200,17 @@ namespace Ombi.UI.Modules return await ProcessMovies(MovieSearchType.Search, searchTerm); } + private Response GetTvPoster(int theTvDbId) + { + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + + var banner = result.image?.medium; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + return banner; + } private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) { List apiMovies; @@ -322,6 +343,186 @@ namespace Ombi.UI.Modules return true; } + private async Task ProcessShows(ShowSearchType type) + { + var shows = new List(); + var prSettings = await PrService.GetSettingsAsync(); + switch (type) + { + case ShowSearchType.Popular: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var popularShows = await TraktApi.GetPopularShows(); + + foreach (var popularShow in popularShows) + { + var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = popularShow.Ids.Imdb, + Network = popularShow.Network, + Overview = popularShow.Overview.RemoveHtml(), + Rating = popularShow.Rating.ToString(), + Runtime = popularShow.Runtime.ToString(), + SeriesName = popularShow.Title, + Status = popularShow.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = popularShow.Trailer, + Homepage = popularShow.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Anticipated: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var anticipated = await TraktApi.GetAnticipatedShows(); + foreach (var anticipatedShow in anticipated) + { + var show = anticipatedShow.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network ?? string.Empty, + Overview = show.Overview?.RemoveHtml() ?? string.Empty, + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status?.DisplayName ?? string.Empty, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.MostWatched: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var mostWatched = await TraktApi.GetMostWatchesShows(); + foreach (var watched in mostWatched) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Trending: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var trending = await TraktApi.GetTrendingShows(); + foreach (var watched in trending) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + + return Response.AsJson(shows); + } + + private async Task> MapToTvModel(List shows, PlexRequestSettings prSettings) + { + + var plexSettings = await PlexService.GetSettingsAsync(); + + var providerId = string.Empty; + // Get the requests + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.TvShow); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbTv = distinctResults.ToDictionary(x => x.ProviderId); + + // Check the external applications + var sonarrCached = SonarrCacher.QueuedIds().ToList(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var content = PlexContentRepository.GetAll(); + var plexTvShows = Checker.GetPlexTvShows(content).ToList(); + + foreach (var show in shows) + { + if (plexSettings.AdvancedSearch) + { + providerId = show.Id.ToString(); + } + + var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4), + providerId); + if (plexShow != null) + { + show.Available = true; + show.PlexUrl = plexShow.Url; + } + else + { + if (dbTv.ContainsKey(show.Id)) + { + var dbt = dbTv[show.Id]; + + show.Requested = true; + show.Episodes = dbt.Episodes.ToList(); + show.Approved = dbt.Approved; + } + if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id)) + // compare to the sonarr/sickrage db + { + show.Requested = true; + } + } + } + return shows; + } + private async Task SearchTvShow(string searchTerm) { @@ -1401,5 +1602,13 @@ namespace Ombi.UI.Modules return false; } } + + private enum ShowSearchType + { + Popular, + Anticipated, + MostWatched, + Trending + } } } diff --git a/Ombi.UI/NinjectModules/ApiModule.cs b/Ombi.UI/NinjectModules/ApiModule.cs index 873a3c527..1a45764c7 100644 --- a/Ombi.UI/NinjectModules/ApiModule.cs +++ b/Ombi.UI/NinjectModules/ApiModule.cs @@ -49,6 +49,7 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); + Bind().To(); } } } \ No newline at end of file diff --git a/Ombi.UI/Ombi.UI.csproj b/Ombi.UI/Ombi.UI.csproj index ce3f1453b..22dd6175d 100644 --- a/Ombi.UI/Ombi.UI.csproj +++ b/Ombi.UI/Ombi.UI.csproj @@ -206,6 +206,10 @@ ..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll True + + ..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll + True + diff --git a/Ombi.UI/Views/Search/Index.cshtml b/Ombi.UI/Views/Search/Index.cshtml index 59fc41464..ec31261e1 100644 --- a/Ombi.UI/Views/Search/Index.cshtml +++ b/Ombi.UI/Views/Search/Index.cshtml @@ -75,9 +75,20 @@ } - + + +
+ - - + -