diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 442359f0f..5a3624c5e 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -28,5 +28,6 @@ namespace Ombi.Core.Engine.Interfaces Task GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted); Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); Task> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); + Task> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index f3023c848..b29171eaf 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -19,6 +19,7 @@ using Ombi.Store.Repository; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -29,7 +30,7 @@ namespace Ombi.Core.Engine.V2 { public MovieSearchEngineV2(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, ILogger logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService s, IRepository sub, - ISettingsService customizationSettings, IMovieRequestEngine movieRequestEngine) + ISettingsService customizationSettings, IMovieRequestEngine movieRequestEngine, IHttpClientFactory httpClientFactory) : base(identity, service, r, um, mem, s, sub) { MovieApi = movApi; @@ -37,6 +38,7 @@ namespace Ombi.Core.Engine.V2 Logger = logger; _customizationSettings = customizationSettings; _movieRequestEngine = movieRequestEngine; + _client = httpClientFactory.CreateClient(); } private IMovieDbApi MovieApi { get; } @@ -44,6 +46,7 @@ namespace Ombi.Core.Engine.V2 private ILogger Logger { get; } private readonly ISettingsService _customizationSettings; private readonly IMovieRequestEngine _movieRequestEngine; + private readonly HttpClient _client; public async Task GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null) { @@ -190,6 +193,30 @@ namespace Ombi.Core.Engine.V2 return await TransformMovieResultsToResponse(results); } + public async Task> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + + var result = await _client.GetAsync("https://raw.githubusercontent.com/Ombi-app/Ombi.News/main/Seasonal.md"); + var keyWordIds = await result.Content.ReadAsStringAsync(); + + if (string.IsNullOrEmpty(keyWordIds)) + { + return new List(); + } + + var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await Cache.GetOrAdd(nameof(SeasonalList) + pagesToLoad.Page + langCode + keyWordIds, + async () => await MovieApi.GetMoviesViaKeywords(keyWordIds, langCode, cancellationToken, pagesToLoad.Page), DateTime.Now.AddHours(12)); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + /// /// Gets recently requested movies /// diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index fb15aab5f..7a0f7e385 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -18,6 +18,7 @@ namespace Ombi.Api.TheMovieDb Task> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task> SearchMovie(string searchTerm, int? year, string languageCode); + Task> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null); Task> SearchTv(string searchTerm, string year = default); Task> TopRated(string languageCode, int? page = null); Task> Upcoming(string languageCode, int? page = null); diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index c5a290e04..70e4a94c7 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -331,6 +331,32 @@ namespace Ombi.Api.TheMovieDb return await Api.Request(request, token); } + public async Task> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null) + { + var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); + request.AddQueryString("sort_by", "vote_average.desc"); + + request.AddQueryString("with_keywords", keywordId); + + // `vote_count` consideration isn't explicitly documented, but using only the `sort_by` filter + // does not provide the same results as `/movie/top_rated`. This appears to be adequate enough + // to filter out extremely high-rated movies due to very little votes + request.AddQueryString("vote_count.gte", "250"); + + if (page != null) + { + request.AddQueryString("page", page.ToString()); + } + + await AddDiscoverSettings(request); + await AddGenreFilter(request, "movie"); + AddRetry(request); + var result = await Api.Request>(request, cancellationToken); + return Mapper.Map>(result.results); + } + public async Task> SearchKeyword(string searchTerm) { var request = new Request("search/keyword", BaseUri, HttpMethod.Get); diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html index 73e68336e..aa9ffc5f2 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html @@ -1,4 +1,4 @@ -
+
{{'Discovery.Combined' | translate}} {{'Discovery.Movies' | translate}} diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts index 2c397e12e..b45f0ccbb 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from "@angular/core"; import { DiscoverOption, IDiscoverCardResult } from "../../interfaces"; import { ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces"; import { SearchV2Service } from "../../../services"; @@ -11,6 +11,7 @@ export enum DiscoverType { Trending, Popular, RecentlyRequested, + Seasonal, } @Component({ @@ -23,6 +24,7 @@ export class CarouselListComponent implements OnInit { @Input() public discoverType: DiscoverType; @Input() public id: string; @Input() public isAdmin: boolean; + @Output() public movieCount: EventEmitter = new EventEmitter(); @ViewChild('carousel', {static: false}) carousel: Carousel; public DiscoverOption = DiscoverOption; @@ -33,6 +35,7 @@ export class CarouselListComponent implements OnInit { public responsiveOptions: any; public RequestType = RequestType; public loadingFlag: boolean; + public DiscoverType = DiscoverType; get mediaTypeStorageKey() { return "DiscoverOptions" + this.discoverType.toString(); @@ -220,7 +223,10 @@ export class CarouselListComponent implements OnInit { break case DiscoverType.RecentlyRequested: this.movies = await this.searchService.recentlyRequestedMoviesByPage(this.currentlyLoaded, this.amountToLoad); + case DiscoverType.Seasonal: + this.movies = await this.searchService.seasonalMoviesByPage(this.currentlyLoaded, this.amountToLoad); } + this.movieCount.emit(this.movies.length); this.currentlyLoaded += this.amountToLoad; } diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html index 701a6b204..89f5dda07 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html @@ -1,4 +1,12 @@
+ +
+

{{'Discovery.SeasonalTab' | translate}}

+
+ +
+
+

{{'Discovery.PopularTab' | translate}}

diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts index ac5a6a9dc..155a69cef 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from "@angular/core"; + import { AuthService } from "../../../auth/auth.service"; import { DiscoverType } from "../carousel-list/carousel-list.component"; @@ -11,6 +12,7 @@ export class DiscoverComponent implements OnInit { public DiscoverType = DiscoverType; public isAdmin: boolean; + public showSeasonal: boolean; constructor(private authService: AuthService) { } @@ -18,4 +20,9 @@ export class DiscoverComponent implements OnInit { this.isAdmin = this.authService.isAdmin(); } + public setSeasonalMovieCount(count: number) { + if (count > 0) { + this.showSeasonal = true; + } + } } diff --git a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts index f7c287fd2..ece4a26c9 100644 --- a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts @@ -63,6 +63,10 @@ export class SearchV2Service extends ServiceHelpers { return this.http.get(`${this.url}/Movie/requested/${currentlyLoaded}/${toLoad}`).toPromise(); } + public seasonalMoviesByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Movie/seasonal/${currentlyLoaded}/${toLoad}`).toPromise(); + } + public nowPlayingMovies(): Observable { return this.http.get(`${this.url}/Movie/nowplaying`); } diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index 4fbdf50f1..6d911ec71 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -164,6 +164,19 @@ namespace Ombi.Controllers.V2 return await _movieEngineV2.PopularMovies(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); } + /// + /// Returns Seasonal Movies + /// + /// We use TheMovieDb as the Movie Provider + /// + [HttpGet("movie/seasonal/{currentPosition}/{amountToLoad}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public async Task> Seasonal(int currentPosition, int amountToLoad) + { + return await _movieEngineV2.SeasonalList(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + } + /// /// Returns Recently Requested Movies using Paging /// diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index 853927d4f..b497c784e 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -295,6 +295,7 @@ "PopularTab": "Popular", "TrendingTab": "Trending", "UpcomingTab": "Upcoming", + "SeasonalTab": "Seasonal", "RecentlyRequestedTab": "Recently Requested", "Movies": "Movies", "Combined": "Combined",