From d543d9894b5298c7d4aa0cbd8f6329454a84fc5f Mon Sep 17 00:00:00 2001 From: kindler chase Date: Tue, 16 Aug 2022 09:18:40 -0500 Subject: [PATCH] Finalize Discover and Get Recommended Movies from PR's #45 & #46 (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * get recommended and similar movies (#45) * get recommended and similar movies * make necessary changes * Fix formatting and whitespace. This follows the repo's already defined code styles. * Shore up the tests for movie recommendations. Add more tests for MovieInfo in the util. * Discover movies (#46) * create discover request api * create discover movie parameter builders * indentation changes * test discover movies * seperate movie parameter builder * remove extra lines and add new line * add editor config file Co-authored-by: Barış Can YILMAZ Co-authored-by: kindler chase * Fix formatting and whitespace. This follows the repo's already defined code styles. * Fix class name spelling error. * Add error checking in DiscoverMoviesAsync. Add error checking in DiscoverMoviesAsync; update conventions to follow repo conventions. * Fix spelling error in method name. * Delete unneeded parameter builder; should be on the main interface. * Update method name for clarity. * Clean up parameter builder with reusable method. * Improve discover movie tests. Improve discover movie tests. * Use the response util to validate results. * Follow conventions of repo styles * Fix copy/paste variable names to be correct names. * Fix method names to reflect actual intent. * Set the default date converter with Epoch time as default date. Set the default date converter with Epoch time as default date. Sometimes the MovieInfo json is missing the release date, so add the expected pre-defined value as the default value. * Modify ApiRequestBase to set the default json serialization settings. Modify ApiRequestBase to set the default json serialization settings. This is a one time operation that all deserialization will use. Clean up the overloads a bit as well. * Add documentation to the IApiDiscoverRequest; update param name. * Fix broken test due to new IApiDiscoverRequest. * Fix broken integration test. Fix broken integration test. Apparently Milla is no longer known for zoolander. * Fix integration test for correct updated count for IApiRequest objects. * Prefer arrays over lists. * Improve GetSimilar tests. * Simplify AssertCanPageSearchResponse. Simplify AssertCanPageSearchResponse. No need to keep track of every single search result duplicated; just need to track the overall duplicate results. * Show duplicate results for GetSimilarAsync_CanPage. Lower dup threshold. * Write to the Trace instead of Debug. * Simplify AssertCanPageSearchResponse by removing min total results param. Simplify AssertCanPageSearchResponse by removing min total results param. The api always returns results with a page size of 20. Just calc the expected size in the util and not as a param. Co-authored-by: Barış Can Yılmaz Co-authored-by: Barış Can YILMAZ --- .../ApiResponseUtil.cs | 107 +++++++------ .../Companies/ApiCompanyRequestTests.cs | 3 +- .../Discover/ApiDiscoverRequestTests.cs | 146 ++++++++++++++++++ .../MovieDb/Genres/ApiGenreRequestTests.cs | 17 +- .../MovieDb/Movies/ApiMovieRequestTests.cs | 27 ++-- .../Movies/ApiMovieRequestTests_GetCredits.cs | 2 +- ...ApiMovieRequestTests_GetRecommendations.cs | 45 ++++++ .../Movies/ApiMovieRequestTests_GetSimilar.cs | 98 ++++++++++++ .../MovieDb/People/ApiPeopleRequestTests.cs | 5 +- .../MovieDb/TV/ApiTVShowRequestTests.cs | 9 +- .../MovieDbFactoryTests.cs | 7 +- DM.MovieApi/ApiRequest/ApiRequestBase.cs | 30 ++-- .../ApiRequest/IsoDateTimeConverterEx.cs | 4 +- DM.MovieApi/IMovieDbApi.cs | 8 +- .../MovieDb/Discover/ApiDiscoverRequest.cs | 41 +++++ .../Discover/DiscoverMovieParameterBuilder.cs | 74 +++++++++ .../MovieDb/Discover/IApiDiscoverRequest.cs | 21 +++ .../IDiscoverMovieParameterBuilder.cs | 38 +++++ DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs | 40 +++++ .../MovieDb/Movies/IApiMovieRequest.cs | 19 ++- DM.MovieApi/MovieDb/Movies/MovieInfo.cs | 3 +- 21 files changed, 637 insertions(+), 107 deletions(-) create mode 100644 DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs create mode 100644 DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs create mode 100644 DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs create mode 100644 DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs create mode 100644 DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs create mode 100644 DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs create mode 100644 DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs diff --git a/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs b/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs index 5f480c7..c66863b 100644 --- a/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs +++ b/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs @@ -17,6 +17,7 @@ internal static class ApiResponseUtil { internal const int TestInitThrottle = 375; internal const int PagingThrottle = 225; + private static readonly DateTime MinDate = new( 1900, 1, 1 ); /// /// Slows down the starting of tests to keep themoviedb.org api from denying the request @@ -29,12 +30,14 @@ public static void ThrottleTests() public static void AssertErrorIsNull( ApiResponseBase response ) { - Console.WriteLine( response.CommandText ); + Log( response.CommandText ); Assert.IsNull( response.Error, response.Error?.ToString() ?? "Makes Compiler Happy" ); } public static void AssertImagePath( string path ) { + if( path == null ) return; + Assert.IsTrue( path.StartsWith( "/" ), $"Actual: {path}" ); Assert.IsTrue( @@ -42,22 +45,35 @@ public static void AssertImagePath( string path ) $"Actual: {path}" ); } - public static async Task AssertCanPageSearchResponse( TSearch search, int minimumPageCount, int minimumTotalResultsCount, - Func>> apiSearch, Func keySelector ) + public static void AssertNoSearchResults( ApiSearchResponse response ) + { + AssertErrorIsNull( response ); + + Assert.AreEqual( 0, response.Results.Count, $"Actual: {response}" ); + Assert.AreEqual( 1, response.PageNumber, $"Actual: {response}" ); + Assert.AreEqual( 0, response.TotalPages, $"Actual: {response}" ); + Assert.AreEqual( 0, response.TotalResults, $"Actual: {response}" ); + } + + public static async Task AssertCanPageSearchResponse( + TSearch search, + int minimumPageCount, + Func>> apiSearch, + Func keySelector ) { if( minimumPageCount < 2 ) { Assert.Fail( "minimumPageCount must be greater than 1." ); } - var allFound = new List(); + var ids = new HashSet(); + var dups = new List(); + int totalResults = 0; int pageNumber = 1; - var priorResults = new Dictionary(); - do { - System.Diagnostics.Trace.WriteLine( $"search: {search} | page: {pageNumber}", "ApiResponseUti.AssertCanPageSearchResponse" ); + Log( $"search: {search} | page: {pageNumber}", "AssertCanPage" ); ApiSearchResponse response = await apiSearch( search, pageNumber ); AssertErrorIsNull( response ); @@ -66,41 +82,28 @@ public static async Task AssertCanPageSearchResponse( TSearch search if( typeof( T ) == typeof( Movie ) ) { - AssertMovieStructure( ( IEnumerable )response.Results ); + AssertMovieStructure( (IEnumerable)response.Results ); } else if( typeof( T ) == typeof( PersonInfo ) ) { - AssertPersonInfoStructure( ( IEnumerable )response.Results ); + AssertPersonInfoStructure( (IEnumerable)response.Results ); } if( keySelector == null ) { - allFound.AddRange( response.Results ); + totalResults += response.Results.Count; } else { - var current = new List(); foreach( T res in response.Results ) { + totalResults++; int key = keySelector( res ); - - if( priorResults.TryAdd( key, 1 ) ) - { - current.Add( res ); - continue; - } - - System.Diagnostics.Trace.WriteLine( $"dup on page {response.PageNumber}: {res}" ); - - if( ++priorResults[key] > 2 ) + if( ids.Add( key ) == false ) { - Assert.Fail( "Every now and then themoviedb.org API returns a duplicate from a prior page. " + - "But this time it exceeded our tolerance of one dup.\r\n" + - $"dup: {res}" ); + dups.Add( res.ToString() ); } } - - allFound.AddRange( current ); } Assert.AreEqual( pageNumber, response.PageNumber ); @@ -108,16 +111,17 @@ public static async Task AssertCanPageSearchResponse( TSearch search Assert.IsTrue( response.TotalPages >= minimumPageCount, $"Expected minimum of {minimumPageCount} TotalPages. Actual TotalPages: {response.TotalPages}" ); - pageNumber++; - // keeps the system from being throttled System.Threading.Thread.Sleep( PagingThrottle ); - } while( pageNumber <= minimumPageCount ); + } while( ++pageNumber <= minimumPageCount ); // will be 1 greater than minimumPageCount in the last loop - Assert.AreEqual( minimumPageCount + 1, pageNumber ); + Assert.AreEqual( minimumPageCount, --pageNumber ); - Assert.IsTrue( allFound.Count >= minimumTotalResultsCount, $"Actual found count: {allFound.Count} | Expected min count: {minimumTotalResultsCount}" ); + // 20 results per page + int minCount = pageNumber * 20; + Assert.IsTrue( totalResults >= minCount, + $"Actual results: {totalResults} | Expected min: {minCount}" ); if( keySelector == null ) { @@ -126,16 +130,21 @@ public static async Task AssertCanPageSearchResponse( TSearch search return; } - List> groupById = allFound - .ToLookup( keySelector ) - .ToList(); - - List dups = groupById - .Where( x => x.Skip( 1 ).Any() ) - .Select( x => $"({x.Count()}) {string.Join( " | ", x.Select( y => y.ToString() ) )}" ) - .ToList(); + // api tends to return duplicate results when paging + // shouldn't be more than 2 or 3 per page; at 20 per page, + // that's approximately 4-6; let's target 20% + int min = (int)(totalResults * 0.8); + var d = dups + .GroupBy( x => x ) + .Select( x => $"{x.Key} (x {x.Count()})" ); + Log( $"Results: {totalResults}, Dups: {dups.Count}\r\n{string.Join( "\r\n", d )}" ); - Assert.AreEqual( 0, dups.Count, "Duplicates: " + Environment.NewLine + string.Join( Environment.NewLine, dups ) ); + if( min >= ids.Count ) + { + Assert.Fail( "Every now and then themoviedb.org API returns a duplicate from a prior page. " + + "But this time it exceeded our tolerance of 20% dups.\r\n" + + $"Actual: {ids.Count} vs {min}" ); + } } private static void AssertPersonInfoStructure( IEnumerable people ) @@ -232,9 +241,15 @@ public static void AssertMovieInformationStructure( IEnumerable movie public static void AssertMovieInformationStructure( MovieInfo movie ) { - Assert.IsFalse( string.IsNullOrWhiteSpace( movie.Title ) ); - Assert.IsTrue( movie.Id > 0 ); + Assert.IsFalse( string.IsNullOrWhiteSpace( movie.Title ), $"Actual: {movie}" ); + Assert.IsFalse( string.IsNullOrWhiteSpace( movie.OriginalTitle ), $"Actual {movie}" ); + // movie.Overview is sometimes empty + + Assert.IsTrue( movie.Id > 1 ); + Assert.IsTrue( movie.ReleaseDate > MinDate, $"Actual: {movie.ReleaseDate} | {movie}" ); + AssertImagePath( movie.BackdropPath ); + AssertImagePath( movie.PosterPath ); AssertGenres( movie.GenreIds, movie.Genres ); } @@ -333,10 +348,10 @@ private static void AssertTvShowGuestStarsStructure( GuestStars guestStars ) Assert.IsFalse( string.IsNullOrWhiteSpace( guestStars.OriginalName ) ); Assert.IsTrue( guestStars.Popularity > 0 ); - if( guestStars.ProfilePath != null ) - { - AssertImagePath( guestStars.ProfilePath ); - } + AssertImagePath( guestStars.ProfilePath ); } + + private static void Log( string msg, string category = null ) + => System.Diagnostics.Trace.WriteLine( msg, category ); } } diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs index c72cbc7..78edc3a 100644 --- a/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs +++ b/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs @@ -68,9 +68,8 @@ public async Task GetMoviesAsync_CanPageResults() { const int companyId = 3; const int minimumPageCount = 5; - const int minimumTotalResultsCount = 95; - await ApiResponseUtil.AssertCanPageSearchResponse( companyId, minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( companyId, minimumPageCount, ( id, pageNumber ) => _api.GetMoviesAsync( id, pageNumber ), x => x.Id ); } } diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs new file mode 100644 index 0000000..9db14ab --- /dev/null +++ b/DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs @@ -0,0 +1,146 @@ +using System.Linq; +using System.Threading.Tasks; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Discover; +using DM.MovieApi.MovieDb.Movies; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DM.MovieApi.IntegrationTests.MovieDb.Discover +{ + [TestClass] + public class ApiDiscoverRequestTests + { + private IApiDiscoverRequest _api; + + [TestInitialize] + public void TestInit() + { + ApiResponseUtil.ThrottleTests(); + + _api = MovieDbFactory.Create().Value; + + Assert.IsInstanceOfType( _api, typeof( ApiDiscoverRequest ) ); + } + + [TestMethod] + public async Task DiscoverMovies_WithCrew() + { + int directorId = 66212; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithCrew( directorId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + } + + [TestMethod] + public async Task DiscoverMovies_WithCrew_HasNoResult_InvalidPersonId() + { + int personId = 0; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithCrew( personId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertNoSearchResults( response ); + } + + [TestMethod] + public async Task DiscoverMovies_WithCast() + { + int actorId = 66462; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithCast( actorId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + } + + [TestMethod] + public async Task DiscoverMovies_WithCast_HasNoResult_InvalidPersonId() + { + int personId = 0; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithCast( personId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertNoSearchResults( response ); + } + + [TestMethod] + public async Task DiscoverMovies_WithGenre() + { + int genreId = 28; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithGenre( genreId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + + Assert.IsTrue( response.Results + .All( r => r.Genres.Any( g => g.Id == genreId ) ), "No results with genre" ); + } + + [TestMethod] + public async Task DiscoverMovies_ExcludeGenre() + { + int genreId = 28; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.ExcludeGenre( genreId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + + Assert.IsTrue( response.Results + .All( r => r.Genres.All( g => g.Id != genreId ) ), "Genre found in results" ); + } + + [TestMethod] + public async Task DiscoverMovies_WithOriginalLanguage_InFinnish() + { + int directorId = 66212; + string originalLanguage = "fi"; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithOriginalLanguage( originalLanguage ).WithCrew( directorId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + } + + [TestMethod] + public async Task DiscoverMovies_WithOriginalLanguage_InGerman() + { + int directorId = 66212; + string originalLanguage = "de"; + + IDiscoverMovieParameterBuilder builder = CreateBuilder(); + builder.WithOriginalLanguage( originalLanguage ).WithCrew( directorId ); + + ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + } + + private IDiscoverMovieParameterBuilder CreateBuilder() + => new DiscoverMovieParameterBuilder(); + } +} diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs index fd8cfb0..d406f2b 100644 --- a/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs +++ b/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs @@ -31,7 +31,7 @@ public async Task FindById_Foreign_Genre_NoLongerExists() const int id = 10769; const string name = "Foreign"; - CollectionAssert.DoesNotContain( _api.AllGenres.ToList(), new Genre( id, name ) ); + CollectionAssert.DoesNotContain( _api.AllGenres.ToArray(), new Genre( id, name ) ); // FindById will add to AllGenres when it does not exist ApiQueryResponse response = await _api.FindByIdAsync( id ); @@ -41,7 +41,7 @@ public async Task FindById_Foreign_Genre_NoLongerExists() // the genre should not have been added to AllGenres since TMDB no longer recognizes // the foreign genre category. - CollectionAssert.DoesNotContain( _api.AllGenres.ToList(), new Genre( id, name ) ); + CollectionAssert.DoesNotContain( _api.AllGenres.ToArray(), new Genre( id, name ) ); } [TestMethod] @@ -78,7 +78,7 @@ public async Task GetAllAsync_Returns_Known_Genres() IReadOnlyList knownGenres = GenreFactory.GetAll(); - CollectionAssert.AreEquivalent( knownGenres.ToList(), response.Item.ToList() ); + CollectionAssert.AreEquivalent( knownGenres.ToArray(), response.Item.ToArray() ); } [TestMethod] @@ -118,7 +118,7 @@ public async Task GetMoviesAsync_IsSubset_OfGetAll() Assert.IsTrue( all.Item.Count > movies.Item.Count ); - CollectionAssert.IsSubsetOf( movies.Item.ToList(), all.Item.ToList() ); + CollectionAssert.IsSubsetOf( movies.Item.ToArray(), all.Item.ToArray() ); } [TestMethod] @@ -134,7 +134,7 @@ public async Task GetTelevisionAsync_IsSubset_OfGetAll() Assert.IsTrue( all.Item.Count > tv.Item.Count ); - CollectionAssert.IsSubsetOf( tv.Item.ToList(), all.Item.ToList() ); + CollectionAssert.IsSubsetOf( tv.Item.ToArray(), all.Item.ToArray() ); } [TestMethod] @@ -152,7 +152,7 @@ public async Task FindMoviesByIdAsync_Returns_ValidResult() foreach( MovieInfo info in response.Results ) { - CollectionAssert.IsSubsetOf( expectedGenres, info.Genres.ToList() ); + CollectionAssert.IsSubsetOf( expectedGenres, info.Genres.ToArray() ); } } @@ -161,10 +161,9 @@ public async Task FindMoviesByIdAsync_CanPageResults() { int genreId = GenreFactory.Comedy().Id; // Comedy has upwards of 2k pages. - const int minimumPageCount = 5; - const int minimumTotalResultsCount = 100; // 20 results per page x 5 pages = 100 + const int minimumPageCount = 10; - await ApiResponseUtil.AssertCanPageSearchResponse( genreId, minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( genreId, minimumPageCount, ( id, page ) => _api.FindMoviesByIdAsync( id, page ), x => x.Id ); } } diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs index 19e0a7e..1731d93 100644 --- a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs +++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs @@ -108,7 +108,7 @@ private void AssertRunLolaRun( ApiSearchResponse response, string exp Assert.AreEqual( new DateTime( 1998, 03, 03 ), movie.ReleaseDate ); - var expectedGenres = new List + var expectedGenres = new[] { GenreFactory.Action(), GenreFactory.Drama(), @@ -122,9 +122,8 @@ public async Task SearchByTitleAsync_CanPageResults() { const string query = "Harry"; const int minimumPageCount = 8; - const int minimumTotalResultsCount = 150; - await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, ( search, pageNumber ) => _api.SearchByTitleAsync( search, pageNumber ), x => x.Id ); } @@ -202,7 +201,7 @@ public async Task FindByIdAsync_StarWarsTheForceAwakens_Returns_AllValues() ApiResponseUtil.AssertImagePath( movie.MovieCollectionInfo.PosterPath ); // Genres - var expectedGenres = new List + var expectedGenres = new[] { GenreFactory.Action(), GenreFactory.Adventure(), @@ -213,15 +212,15 @@ public async Task FindByIdAsync_StarWarsTheForceAwakens_Returns_AllValues() "actual:\r\n" + string.Join( "\r\n", movie.Genres ) ); // Keywords - var expectedKeywords = new List + var expectedKeywords = new Keyword[] { new(803, "android"), new(1612, "spacecraft"), new(161176, "space opera") }; CollectionAssert.AreEquivalent( expectedKeywords, movie.Keywords.ToArray(), - $"\r\nactual:\r\n\t{string.Join( "\r\n\t", expectedKeywords )}" + - $"\r\nactual:\r\n\t{string.Join( "\r\n\t", movie.Keywords )}" ); + $"\r\nactual:\r\n\t{string.Join( "\r\n\t", expectedKeywords.Select( x => x.ToString() ) )}" + + $"\r\nactual:\r\n\t{string.Join( "\r\n\t", movie.Keywords.Select( x => x.ToString() ) )}" ); } [TestMethod] @@ -279,9 +278,8 @@ public async Task GetNowPlayingAsync_CanPageResults() { // Now Playing typically has 25+ pages. const int minimumPageCount = 5; - const int minimumTotalResultsCount = 100; // 20 results per page x 5 pages = 100 - await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, ( _, page ) => _api.GetNowPlayingAsync( page ), x => x.Id ); } @@ -299,11 +297,10 @@ public async Task GetUpcomingAsync_Returns_ValidResults() public async Task GetUpcomingAsync_CanPageResults() { // Now Playing typically has 5+ pages. - // - intentionally setting minimumTotalResultsCount at 50; sometimes upcoming movies are scarce. + // note: sometimes upcoming movies are scarce and may occasionally fail. const int minimumPageCount = 3; - const int minimumTotalResultsCount = 50; // 20 results per page x 3 pages = 60 - await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, ( _, page ) => _api.GetUpcomingAsync( page ), x => x.Id ); } @@ -323,9 +320,8 @@ public async Task GetTopRatedAsync_Returns_ValidResults() public async Task GetTopRatedAsync_CanPageResults() { const int minimumPageCount = 2; - const int minimumTotalResultsCount = 40; - await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, ( _, page ) => _api.GetTopRatedAsync( page ), x => x.Id ); } @@ -345,9 +341,8 @@ public async Task GetPopularAsync_Returns_ValidResults() public async Task GetPopularAsync_CanPageResults() { const int minimumPageCount = 2; - const int minimumTotalResultsCount = 40; - await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, ( _, page ) => _api.GetPopularAsync( page ), x => x.Id ); } } diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs index 4fc4ec4..f9d11f0 100644 --- a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs +++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading.Tasks; using DM.MovieApi.ApiResponse; using DM.MovieApi.MovieDb.Movies; diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs new file mode 100644 index 0000000..f995741 --- /dev/null +++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Movies; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DM.MovieApi.IntegrationTests.MovieDb.Movies +{ + [TestClass] + public class GetRecommendationsTests + { + private IApiMovieRequest _api; + + [TestInitialize] + public void TestInit() + { + ApiResponseUtil.ThrottleTests(); + + _api = MovieDbFactory.Create().Value; + } + + [TestMethod] + public async Task GetRecommendationsAsync_Returns_ValidResults() + { + const int movieIdRunLolaRun = 104; + + ApiSearchResponse response = await _api.GetRecommendationsAsync( movieIdRunLolaRun ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + + Assert.IsTrue( response.TotalPages > 1 ); + Assert.IsTrue( response.TotalResults > 20 ); + Assert.AreEqual( 1, response.PageNumber ); + } + + [TestMethod] + public async Task GetRecommendationsAsync_HasError_InvalidMovieId() + { + const int movieId = 1; + + ApiSearchResponse response = await _api.GetRecommendationsAsync( movieId ); + Assert.IsNotNull( response.Error ); + } + } +} diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs new file mode 100644 index 0000000..dd9e7cb --- /dev/null +++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Movies; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DM.MovieApi.IntegrationTests.MovieDb.Movies +{ + [TestClass] + public class GetSimilarTests + { + private IApiMovieRequest _api; + + [TestInitialize] + public void TestInit() + { + ApiResponseUtil.ThrottleTests(); + + _api = MovieDbFactory.Create().Value; + } + + [TestMethod] + public async Task GetSimilarAsync_Returns_ValidResults() + { + const int movieIdRunLolaRun = 104; + + ApiSearchResponse response = await _api.GetSimilarAsync( movieIdRunLolaRun ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + + // get similar will return the max number of results + Assert.AreEqual( 20, response.Results.Count ); + Assert.AreEqual( 500, response.TotalPages ); + Assert.AreEqual( 10000, response.TotalResults ); + Assert.AreEqual( 1, response.PageNumber ); + } + + [TestMethod] + public async Task GetSimilarAsync_CanPage() + { + const int movieIdRunLolaRun = 104; + + // todo: move to ApiResponseUtil and refactor w/ existing AssertCanPageSearchResponse + // * each component may be able to become re-usable methods + + int num = 0; + var ids = new HashSet(); + var dups = new List(); + + for( int i = 1; i <= 10; i++ ) + { + int pageNumber = i; + ApiSearchResponse response = await _api.GetSimilarAsync( movieIdRunLolaRun, pageNumber ); + + ApiResponseUtil.AssertErrorIsNull( response ); + ApiResponseUtil.AssertMovieInformationStructure( response.Results ); + + // get similar will return the max number of results + Assert.AreEqual( 20, response.Results.Count ); + Assert.AreEqual( 500, response.TotalPages ); + Assert.AreEqual( 10000, response.TotalResults ); + Assert.AreEqual( pageNumber, response.PageNumber ); + + foreach( MovieInfo m in response.Results ) + { + num++; + if( ids.Add( m.Id ) == false ) + { + dups.Add( m.ToString() ); + } + } + } + + // api tends to return duplicate results when paging + // shouldn't be more than 2 or 3 per page; at 20 per page, + // that's approximately 4-6; let's target 25% + int min = (int)(num * 0.75); + var d = dups + .GroupBy( x => x ) + .Select( x => $"{x.Key} (x {x.Count()})" ); + System.Diagnostics.Trace.WriteLine( $"Results: {num}, Dups: {dups.Count}" + + $"\r\n{string.Join( "\r\n", d )}" ); + + Assert.IsTrue( ids.Count >= min, $"Total: {num}.\r\nUnique: {ids.Count}, Dup Threshold {min}" ); + } + + [TestMethod] + public async Task GetSimilarAsync_HasError_InvalidMovieId() + { + const int movieId = 1; + + ApiSearchResponse response = await _api.GetSimilarAsync( movieId ); + Assert.IsNotNull( response.Error ); + } + } +} diff --git a/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs index 3041b6a..4632a53 100644 --- a/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs +++ b/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs @@ -310,7 +310,7 @@ public async Task SearchByNameAsync_Milla_Jovovich_Returns_SingleResult_WithExpe { "The Fifth Element", "Resident Evil", - "Zoolander" + "Resident Evil: Apocalypse" }; foreach( string role in roles ) @@ -326,9 +326,8 @@ public async Task SearchByNameAsync_CanPageResults() { const string query = "Cox"; const int minimumPageCount = 15; - const int minimumTotalResultsCount = 300; - await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, ( search, pageNumber ) => _api.SearchByNameAsync( search, pageNumber ), null ); } } diff --git a/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs index f192756..4a5f395 100644 --- a/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs +++ b/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs @@ -100,9 +100,8 @@ public async Task SearchByNameAsync_CanPageResults() { const string query = "full"; const int minimumPageCount = 4; - const int minimumTotalResultsCount = 80; - await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, ( search, pageNumber ) => _api.SearchByNameAsync( search, pageNumber ), x => x.Id ); } @@ -224,9 +223,8 @@ public async Task GetTopRatedAsync_Returns_ValidResult() public async Task GetTopRatedAsync_CanPageResults() { const int minimumPageCount = 5; - const int minimumTotalResultsCount = 100; - await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount, ( _, page ) => _api.GetTopRatedAsync( page ), x => x.Id ); } @@ -246,9 +244,8 @@ public async Task GetPopularAsync_Returns_ValidResult() public async Task GetPopularAsync_CanPageResults() { const int minimumPageCount = 5; - const int minimumTotalResultsCount = 100; - await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount, minimumTotalResultsCount, + await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount, ( _, page ) => _api.GetPopularAsync( page ), x => x.Id ); } diff --git a/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs b/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs index 27a4f67..22f4314 100644 --- a/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs +++ b/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -88,13 +87,13 @@ public void Create_Returns_Lazy_IApiMovieRequest() [TestMethod] public void GetAllApiRequests_CanCreate_IMovieApi() { - List dbApi = typeof( IMovieDbApi ) + PropertyInfo[] dbApi = typeof( IMovieDbApi ) .GetProperties() .Where( x => typeof( IApiRequest ).IsAssignableFrom( x.PropertyType ) ) .Distinct() - .ToList(); + .ToArray(); - Assert.AreEqual( 8, dbApi.Count ); + Assert.AreEqual( 9, dbApi.Length ); IMovieDbApi api; diff --git a/DM.MovieApi/ApiRequest/ApiRequestBase.cs b/DM.MovieApi/ApiRequest/ApiRequestBase.cs index 19ea1c8..1d4e6d0 100644 --- a/DM.MovieApi/ApiRequest/ApiRequestBase.cs +++ b/DM.MovieApi/ApiRequest/ApiRequestBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -14,15 +14,7 @@ internal abstract class ApiRequestBase { private readonly IApiSettings _settings; - protected ApiRequestBase( IApiSettings settings ) - { - _settings = settings; - } - - public async Task> QueryAsync( string command ) - => await QueryAsync( command, new Dictionary() ); - - public async Task> QueryAsync( string command, IDictionary parameters ) + static ApiRequestBase() { var settings = new JsonSerializerSettings { @@ -31,17 +23,25 @@ public async Task> QueryAsync( string command, IDictionar }; settings.Converters.Add( new IsoDateTimeConverterEx() ); - return await QueryAsync( command, parameters, Deserializer ); - - T Deserializer( string json ) - => JsonConvert.DeserializeObject( json, settings ); + JsonConvert.DefaultSettings = () => settings; } + protected ApiRequestBase( IApiSettings settings ) + => _settings = settings; + + public async Task> QueryAsync( string command ) + => await QueryAsync( command, new Dictionary(), null ); + + public async Task> QueryAsync( string command, IDictionary parameters ) + => await QueryAsync( command, parameters, null ); + public async Task> QueryAsync( string command, Func deserializer ) => await QueryAsync( command, new Dictionary(), deserializer ); public async Task> QueryAsync( string command, IDictionary parameters, Func deserializer ) { + deserializer ??= JsonConvert.DeserializeObject; + using HttpClient client = CreateClient(); string cmd = CreateCommand( command, parameters ); @@ -118,7 +118,7 @@ public async Task> SearchAsync( string command, int page return error; } - var result = JsonConvert.DeserializeObject>( json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + var result = JsonConvert.DeserializeObject>( json ); // ReSharper disable PossibleNullReferenceException result.CommandText = response.RequestMessage.RequestUri.ToString(); diff --git a/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs b/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs index a56ea14..805ba5f 100644 --- a/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs +++ b/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs @@ -37,7 +37,7 @@ public override object ReadJson( JsonReader reader, Type objectType, object exis return new DateTime( year, 1, 1 ); } - return default( DateTime ); + return DateTime.UnixEpoch; } } @@ -50,7 +50,7 @@ private void ConditionalTraceReaderValue( JsonReader reader ) val = ""; } - Debug.WriteLine( $"IsoDateTimeConverterEx.JsonReader.Value: {val}" ); + Trace.WriteLine( val, "IsoDateTimeConverterEx" ); } } } diff --git a/DM.MovieApi/IMovieDbApi.cs b/DM.MovieApi/IMovieDbApi.cs index bf525b4..5b1d0bc 100644 --- a/DM.MovieApi/IMovieDbApi.cs +++ b/DM.MovieApi/IMovieDbApi.cs @@ -1,6 +1,7 @@ -using DM.MovieApi.MovieDb.Certifications; +using DM.MovieApi.MovieDb.Certifications; using DM.MovieApi.MovieDb.Companies; using DM.MovieApi.MovieDb.Configuration; +using DM.MovieApi.MovieDb.Discover; using DM.MovieApi.MovieDb.Genres; using DM.MovieApi.MovieDb.IndustryProfessions; using DM.MovieApi.MovieDb.Movies; @@ -54,5 +55,10 @@ public interface IMovieDbApi /// Provides access for retrieving information about People. /// IApiPeopleRequest People { get; } + + /// + /// Provides access for discovering movies based on a filtered set of parameters. + /// + IApiDiscoverRequest Discover { get; } } } diff --git a/DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs b/DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs new file mode 100644 index 0000000..0fb32c3 --- /dev/null +++ b/DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Genres; +using DM.MovieApi.MovieDb.Movies; +using DM.MovieApi.Shims; + +namespace DM.MovieApi.MovieDb.Discover +{ + internal class ApiDiscoverRequest : ApiRequestBase, IApiDiscoverRequest + { + private readonly IApiGenreRequest _genreApi; + + [ImportingConstructor] + public ApiDiscoverRequest( IApiSettings settings, IApiGenreRequest genreApi ) + : base( settings ) + { + _genreApi = genreApi; + } + + public async Task> DiscoverMoviesAsync( IDiscoverMovieParameterBuilder builder, int pageNumber = 1, string language = "en" ) + { + Dictionary param = builder.Build(); + param.Add( "language", language ); + + const string command = "discover/movie"; + + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + } +} diff --git a/DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs b/DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs new file mode 100644 index 0000000..4fc44b2 --- /dev/null +++ b/DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; + +namespace DM.MovieApi.MovieDb.Discover +{ + public class DiscoverMovieParameterBuilder : IDiscoverMovieParameterBuilder + { + private readonly Dictionary> _param; + + public DiscoverMovieParameterBuilder() + { + _param = new Dictionary>(); + } + + public Dictionary Build() + { + var param = new Dictionary(); + + foreach( var kvp in _param ) + { + param.Add( kvp.Key, string.Join( ",", kvp.Value ) ); + } + + return param; + } + + public IDiscoverMovieParameterBuilder WithCast( int personId ) + { + AddParamType( "with_cast" ); + + _param["with_cast"].Add( personId.ToString() ); + + return this; + } + + public IDiscoverMovieParameterBuilder WithCrew( int personId ) + { + AddParamType( "with_crew" ); + + _param["with_crew"].Add( personId.ToString() ); + + return this; + } + + public IDiscoverMovieParameterBuilder WithGenre( int genreId ) + { + AddParamType( "with_genres" ); + + _param["with_genres"].Add( genreId.ToString() ); + + return this; + } + + public IDiscoverMovieParameterBuilder WithOriginalLanguage( string language ) + { + AddParamType( "original_language" ); + + _param["original_language"].Add( language ); + + return this; + } + + public IDiscoverMovieParameterBuilder ExcludeGenre( int genreId ) + { + AddParamType( "without_genres" ); + + _param["without_genres"].Add( genreId.ToString() ); + + return this; + } + + private void AddParamType( string name ) + => _param.TryAdd( name, new List() ); + } +} diff --git a/DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs b/DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs new file mode 100644 index 0000000..29540b5 --- /dev/null +++ b/DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using DM.MovieApi.ApiRequest; +using DM.MovieApi.ApiResponse; +using DM.MovieApi.MovieDb.Movies; + +namespace DM.MovieApi.MovieDb.Discover +{ + /// + /// Interface for discovering movies based on the filter provided by the parameter builder. + /// + public interface IApiDiscoverRequest : IApiRequest + { + /// + /// Allows for the discovery of movies by various types of data provided to the parameter. + /// + /// Provides a method of adding several types of parameters to filter the query. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> DiscoverMoviesAsync( IDiscoverMovieParameterBuilder builder, int pageNumber = 1, string language = "en" ); + } +} diff --git a/DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs b/DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs new file mode 100644 index 0000000..c3efc9e --- /dev/null +++ b/DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace DM.MovieApi.MovieDb.Discover +{ + public interface IDiscoverMovieParameterBuilder + { + /// + /// Builds all parameters for use with an . + /// Typically called by the internal engine. + /// + Dictionary Build(); + + /// + /// Add for each language version to be returned in the query. May be invoked more than once. + /// + IDiscoverMovieParameterBuilder WithOriginalLanguage( string language ); + + /// + /// Add for each crew member to be returned in the query. May be invoked more than once. + /// + IDiscoverMovieParameterBuilder WithCrew( int personId ); + + /// + /// Add for each cast member to be returned in the query. May be invoked more than once. + /// + IDiscoverMovieParameterBuilder WithCast( int personId ); + + /// + /// Add for each genre to be included in the query. May be invoked more than once. + /// + IDiscoverMovieParameterBuilder WithGenre( int genre ); + + /// + /// Add for each genre to be excluded from the query. May be invoked more than once. + /// + IDiscoverMovieParameterBuilder ExcludeGenre( int genre ); + } +} diff --git a/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs b/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs index e7a1d60..ce758a5 100644 --- a/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs +++ b/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs @@ -156,5 +156,45 @@ public async Task> GetCreditsAsync( int movieId, s return response; } + + public async Task> GetRecommendationsAsync( int movieId, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + string command = $"movie/{movieId}/recommendations"; + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } + + public async Task> GetSimilarAsync( int movieId, int pageNumber = 1, string language = "en" ) + { + var param = new Dictionary + { + {"language", language}, + }; + + string command = $"movie/{movieId}/similar"; + ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param ); + + if( response.Error != null ) + { + return response; + } + + response.Results.PopulateGenres( _genreApi ); + + return response; + } } } diff --git a/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs b/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs index f919d5a..ea19faf 100644 --- a/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs +++ b/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using DM.MovieApi.ApiRequest; using DM.MovieApi.ApiResponse; @@ -64,5 +64,22 @@ public interface IApiMovieRequest : IApiRequest /// The movie Id is typically found from a more generic Movie query. /// Default is English. The ISO 639-1 language code to retrieve the result from. Task> GetCreditsAsync( int movieId, string language = "en" ); + + /// + /// Get a list of recommended movies for a movie. + /// + /// The movie Id is typically found from a more generic Movie query. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetRecommendationsAsync( int movieId, int pageNumber = 1, string language = "en" ); + + /// + /// Get a list of similar movies. This is not the same as the "Recommendation" system you see on the website. + /// These items are assembled by looking at keywords and genres. + /// + /// The movie Id is typically found from a more generic Movie query. + /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available. + /// Default is English. The ISO 639-1 language code to retrieve the result from. + Task> GetSimilarAsync( int movieId, int pageNumber = 1, string language = "en" ); } } diff --git a/DM.MovieApi/MovieDb/Movies/MovieInfo.cs b/DM.MovieApi/MovieDb/Movies/MovieInfo.cs index bca1e1f..31e6d93 100644 --- a/DM.MovieApi/MovieDb/Movies/MovieInfo.cs +++ b/DM.MovieApi/MovieDb/Movies/MovieInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; using DM.MovieApi.MovieDb.Genres; @@ -53,6 +53,7 @@ public MovieInfo() { GenreIds = Array.Empty(); Genres = Array.Empty(); + ReleaseDate = DateTime.UnixEpoch; } public override string ToString()