We now have infinite scroll!

Also fixed some more unit tests
This commit is contained in:
tidusjar 2019-04-17 17:19:06 +01:00
parent 4e0459eef0
commit 961ba4297a
11 changed files with 162 additions and 68 deletions

View file

@ -9,6 +9,7 @@ namespace Ombi.Core.Engine.Interfaces
Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm); Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm);
Task<SearchTvShowViewModel> GetShowInformation(int tvdbid); Task<SearchTvShowViewModel> GetShowInformation(int tvdbid);
Task<IEnumerable<SearchTvShowViewModel>> Popular(); Task<IEnumerable<SearchTvShowViewModel>> Popular();
Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad);
Task<IEnumerable<SearchTvShowViewModel>> Anticipated(); Task<IEnumerable<SearchTvShowViewModel>> Anticipated();
Task<IEnumerable<SearchTvShowViewModel>> MostWatches(); Task<IEnumerable<SearchTvShowViewModel>> MostWatches();
Task<IEnumerable<SearchTvShowViewModel>> Trending(); Task<IEnumerable<SearchTvShowViewModel>> Trending();

View file

@ -21,6 +21,7 @@ using Ombi.Core.Authentication;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using TraktApiSharp.Objects.Get.Shows;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -127,6 +128,19 @@ namespace Ombi.Core.Engine
return processed; return processed;
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad)
{
var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit);
var results = new List<TraktShow>();
foreach (var pagesToLoad in pages)
{
var apiResult = await TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit);
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
var processed = ProcessResults(results);
return processed;
}
public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated() public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated()
{ {

View file

@ -98,6 +98,9 @@ namespace Ombi.Core.Engine.V2
return null; return null;
} }
private const int _theMovieDbMaxPageItems = 20;
/// <summary> /// <summary>
/// Gets popular movies by paging /// Gets popular movies by paging
/// </summary> /// </summary>
@ -106,28 +109,15 @@ namespace Ombi.Core.Engine.V2
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
// Pages of 20 var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems);
if(toLoad > 20)
var results = new List<MovieSearchResult>();
foreach (var pagesToLoad in pages)
{ {
throw new ApplicationException("Please load less than or equal to 20 items at a time due to a API limit"); var apiResult = await MovieApi.PopularMovies(langCode, pagesToLoad.Page);
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results);
// TheMovieDb only shows pages of 20, let's work out how many we need to load
var page = Math.Round((decimal)(currentlyLoaded / 10) / 2, 0);
if(page == 0)
{
// First page
}
var result = await MovieApi.PopularMovies(langCode);
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
}
return null;
} }
/// <summary> /// <summary>

View file

@ -14,7 +14,7 @@ namespace Ombi.Helpers.Tests
var pages = result.Select(x => x.Page).ToArray(); var pages = result.Select(x => x.Page).ToArray();
Assert.That(pages.Length, Is.EqualTo(expectedPages.Length), "Did not contain the correct amount of pages"); Assert.That(pages.Length, Is.EqualTo(expectedPages.Length), "Did not contain the correct amount of pages");
for (int i = 0; i < pages.Length; i++) for (var i = 0; i < pages.Length; i++)
{ {
Assert.That(pages[i], Is.EqualTo(expectedPages[i])); Assert.That(pages[i], Is.EqualTo(expectedPages[i]));
} }
@ -37,6 +37,7 @@ namespace Ombi.Helpers.Tests
yield return new TestCaseData(20, 9, 20, new[] { 2 }).SetName("Pagination_Load_LessThan_Half_Second_Page"); yield return new TestCaseData(20, 9, 20, new[] { 2 }).SetName("Pagination_Load_LessThan_Half_Second_Page");
yield return new TestCaseData(30, 10, 20, new[] { 2 }).SetName("Pagination_Load_All_Second_Page_With_Half_Take"); yield return new TestCaseData(30, 10, 20, new[] { 2 }).SetName("Pagination_Load_All_Second_Page_With_Half_Take");
yield return new TestCaseData(49, 1, 50, new[] { 1 }).SetName("Pagination_Load_49_OutOf_50"); yield return new TestCaseData(49, 1, 50, new[] { 1 }).SetName("Pagination_Load_49_OutOf_50");
yield return new TestCaseData(49, 1, 100,new[] { 1 }).SetName("Pagination_Load_50_OutOf_100");
} }
} }
@ -56,7 +57,7 @@ namespace Ombi.Helpers.Tests
yield return new TestCaseData(0, 10, 20, 10, 0).SetName("PaginationPosition_Load_First_Half_Of_Page"); yield return new TestCaseData(0, 10, 20, 10, 0).SetName("PaginationPosition_Load_First_Half_Of_Page");
yield return new TestCaseData(10, 10, 20, 10, 10).SetName("PaginationPosition_Load_EndHalf_First_Page"); yield return new TestCaseData(10, 10, 20, 10, 10).SetName("PaginationPosition_Load_EndHalf_First_Page");
yield return new TestCaseData(19, 1, 20, 1, 19).SetName("PaginationPosition_Load_LastItem_Of_First_Page"); yield return new TestCaseData(19, 1, 20, 1, 19).SetName("PaginationPosition_Load_LastItem_Of_First_Page");
yield return new TestCaseData(20, 20, 20, 20, 20).SetName("PaginationPosition_Load_Full_Second_Page"); yield return new TestCaseData(20, 20, 300, 20, 20).SetName("PaginationPosition_Load_Full_Second_Page");
} }
} }
@ -88,6 +89,16 @@ namespace Ombi.Helpers.Tests
.SetName("PaginationPosition_Load_EndFirstPage_Full_SecondPage"); .SetName("PaginationPosition_Load_EndFirstPage_Full_SecondPage");
yield return new TestCaseData(38, 4, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(2, 2, 18), new MultiplePagesTestData(3, 2, 0) }) yield return new TestCaseData(38, 4, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(2, 2, 18), new MultiplePagesTestData(3, 2, 0) })
.SetName("PaginationPosition_Load_EndSecondPage_Some_ThirdPage"); .SetName("PaginationPosition_Load_EndSecondPage_Some_ThirdPage");
yield return new TestCaseData(15, 20, 10, new List<MultiplePagesTestData> { new MultiplePagesTestData(2, 5, 5), new MultiplePagesTestData(3, 10, 0), new MultiplePagesTestData(4, 5, 0) })
.SetName("PaginationPosition_Load_EndSecondPage_All_ThirdPage_Some_ForthPage");
yield return new TestCaseData(24, 12, 12, new List<MultiplePagesTestData> { new MultiplePagesTestData(3, 12, 0) })
.SetName("PaginationPosition_Load_ThirdPage_Of_12");
yield return new TestCaseData(12, 12, 12, new List<MultiplePagesTestData> { new MultiplePagesTestData(2, 12, 0) })
.SetName("PaginationPosition_Load_SecondPage_Of_12");
yield return new TestCaseData(40, 20, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(3, 20, 0) })
.SetName("PaginationPosition_Load_FullThird_Page");
yield return new TestCaseData(240, 12, 20, new List<MultiplePagesTestData> { new MultiplePagesTestData(13, 12, 0) })
.SetName("PaginationPosition_Load_Page_13");
} }
} }

View file

@ -17,9 +17,13 @@ namespace Ombi.Helpers
var lastPage = lastItemIndex / maxItemsPerPage + 1; var lastPage = lastItemIndex / maxItemsPerPage + 1;
var stopPos = lastItemIndex % maxItemsPerPage + 1; var stopPos = lastItemIndex % maxItemsPerPage + 1;
if (currentlyLoaded > maxItemsPerPage) while (currentlyLoaded > maxItemsPerPage)
{ {
currentlyLoaded = currentlyLoaded - maxItemsPerPage; currentlyLoaded -= maxItemsPerPage;
}
if ((currentlyLoaded % maxItemsPerPage) == 0 && (currentlyLoaded % toTake) == 0)
{
currentlyLoaded = 0;
} }
var page1 = new PagesToLoad { Page = firstPage, Skip = currentlyLoaded, Take = toTake }; var page1 = new PagesToLoad { Page = firstPage, Skip = currentlyLoaded, Take = toTake };
@ -40,6 +44,10 @@ namespace Ombi.Helpers
} }
else else
{ {
if (page1.Skip + page1.Take > maxItemsPerPage)
{
page1.Skip = 0;
}
result.Add(page1); result.Add(page1);
} }

View file

@ -6,6 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="MockQueryable.Moq" Version="1.1.0" />
<PackageReference Include="Moq" Version="4.10.0" /> <PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Nunit" Version="3.11.0" /> <PackageReference Include="Nunit" Version="3.11.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />

View file

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Castle.Components.DictionaryAdapter; using Castle.Components.DictionaryAdapter;
using Hangfire; using Hangfire;
using Moq; using Moq;
using MockQueryable.Moq;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Notifications; using Ombi.Core.Notifications;
using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Plex;
@ -68,7 +69,6 @@ namespace Ombi.Schedule.Tests
} }
[Test] [Test]
[Ignore("EF IAsyncQueryProvider")]
public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex() public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex()
{ {
var request = new ChildRequests var request = new ChildRequests
@ -90,21 +90,25 @@ namespace Ombi.Schedule.Tests
} }
} }
} }
},
RequestedUser = new OmbiUser
{
Email = "abc"
} }
}; };
_tv.Setup(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable()); _tv.Setup(x => x.GetChild()).Returns(new List<ChildRequests> { request }.AsQueryable().BuildMock().Object);
_repo.Setup(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode> _repo.Setup(x => x.GetAllEpisodes()).Returns(new List<PlexEpisode>
{ {
new PlexEpisode new PlexEpisode
{ {
Series = new PlexServerContent Series = new PlexServerContent
{ {
ImdbId = 1.ToString(), TvDbId = 1.ToString(),
}, },
EpisodeNumber = 1, EpisodeNumber = 1,
SeasonNumber = 2 SeasonNumber = 2
} }
}.AsQueryable); }.AsQueryable().BuildMock().Object);
_repo.Setup(x => x.Include(It.IsAny<IQueryable<PlexEpisode>>(),It.IsAny<Expression<Func<PlexEpisode, PlexServerContent>>>())); _repo.Setup(x => x.Include(It.IsAny<IQueryable<PlexEpisode>>(),It.IsAny<Expression<Func<PlexEpisode, PlexServerContent>>>()));
await Checker.Start(); await Checker.Start();

View file

@ -13,9 +13,7 @@
color="primary">{{'Discovery.UpcomingTab' | translate}}</button> color="primary">{{'Discovery.UpcomingTab' | translate}}</button>
</div> </div>
</div> </div>
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner>
</div>
<div *ngIf="discoverResults" class="row full-height discoverResults" <div *ngIf="discoverResults" class="row full-height discoverResults"
infiniteScroll infiniteScroll
[fromRoot]="true" [fromRoot]="true"
@ -25,4 +23,7 @@
<discover-card [result]="result"></discover-card> <discover-card [result]="result"></discover-card>
</div> </div>
</div> </div>
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner>
</div>
</div> </div>

View file

@ -31,28 +31,40 @@ export class DiscoverComponent implements OnInit {
public loadingFlag: boolean; public loadingFlag: boolean;
private contentLoaded: number; private contentLoaded: number;
private isScrolling: boolean = false;
constructor(private searchService: SearchV2Service) { } constructor(private searchService: SearchV2Service) { }
public async ngOnInit() { public async ngOnInit() {
this.loading() this.loading()
this.movies = await this.searchService.popularMovies().toPromise(); this.movies = await this.searchService.popularMoviesByPage(0,12).toPromise();
this.tvShows = await this.searchService.popularTv().toPromise(); this.tvShows = await this.searchService.popularTvByPage(0,12);
this.contentLoaded = 12; this.contentLoaded = 12;
this.createModel(true); this.createInitialModel();
} }
public async onScroll() { public async onScroll() {
if (!this.contentLoaded) {
return;
}
if (!this.isScrolling) {
debugger;
this.isScrolling = true;
console.log("SCROLLED!") console.log("SCROLLED!")
this.loading();
if (this.popularActive) {
this.movies = await this.searchService.popularMoviesByPage(this.contentLoaded, 12).toPromise(); this.movies = await this.searchService.popularMoviesByPage(this.contentLoaded, 12).toPromise();
this.tvShows = []; this.tvShows = await this.searchService.popularTvByPage(this.contentLoaded, 12);
this.contentLoaded += 12; this.contentLoaded += 12;
}
this.createModel(false); this.createModel();
this.isScrolling = false;
}
} }
public async popular() { public async popular() {
@ -63,11 +75,11 @@ export class DiscoverComponent implements OnInit {
this.popularActive = true; this.popularActive = true;
this.trendingActive = false; this.trendingActive = false;
this.upcomingActive = false; this.upcomingActive = false;
this.movies = await this.searchService.popularMovies().toPromise(); this.movies = await this.searchService.popularMoviesByPage(0, 12).toPromise();
this.tvShows = await this.searchService.popularTv().toPromise(); this.tvShows = await this.searchService.popularTvByPage(0, 12);
this.createModel(true); this.createModel();
} }
public async trending() { public async trending() {
@ -81,7 +93,7 @@ export class DiscoverComponent implements OnInit {
this.movies = await this.searchService.nowPlayingMovies().toPromise(); this.movies = await this.searchService.nowPlayingMovies().toPromise();
this.tvShows = await this.searchService.trendingTv().toPromise(); this.tvShows = await this.searchService.trendingTv().toPromise();
this.createModel(true); this.createModel();
} }
public async upcoming() { public async upcoming() {
@ -94,11 +106,46 @@ export class DiscoverComponent implements OnInit {
this.movies = await this.searchService.upcomingMovies().toPromise(); this.movies = await this.searchService.upcomingMovies().toPromise();
this.tvShows = await this.searchService.anticipatedTv().toPromise(); this.tvShows = await this.searchService.anticipatedTv().toPromise();
this.createModel(true); this.createModel();
} }
private createModel(shuffle: boolean) { private createModel() {
const tempResults = <IDiscoverCardResult[]>[];
this.movies.forEach(m => {
tempResults.push({
available: m.available,
posterPath: `https://image.tmdb.org/t/p/w300/${m.posterPath}`,
requested: m.requested,
title: m.title,
type: RequestType.movie,
id: m.id,
url: `http://www.imdb.com/title/${m.imdbId}/`,
rating: m.voteAverage,
overview: m.overview,
approved: m.approved
});
});
this.tvShows.forEach(m => {
tempResults.push({
available: m.available,
posterPath: "../../../images/default_tv_poster.png",
requested: m.requested,
title: m.title,
type: RequestType.tvShow,
id: m.id,
url: undefined,
rating: +m.rating,
overview: m.overview,
approved: m.approved
});
});
this.shuffle(tempResults);
this.discoverResults.push(...tempResults);
this.finishLoading(); this.finishLoading();
}
private createInitialModel() {
this.movies.forEach(m => { this.movies.forEach(m => {
this.discoverResults.push({ this.discoverResults.push({
available: m.available, available: m.available,
@ -127,9 +174,8 @@ export class DiscoverComponent implements OnInit {
approved: m.approved approved: m.approved
}); });
}); });
if(shuffle) {
this.shuffle(this.discoverResults); this.shuffle(this.discoverResults);
} this.finishLoading();
} }
private shuffle(discover: IDiscoverCardResult[]): IDiscoverCardResult[] { private shuffle(discover: IDiscoverCardResult[]): IDiscoverCardResult[] {

View file

@ -53,6 +53,11 @@ export class SearchV2Service extends ServiceHelpers {
public popularTv(): Observable<ISearchTvResult[]> { public popularTv(): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/popular`, { headers: this.headers }); return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/popular`, { headers: this.headers });
} }
public popularTvByPage(currentlyLoaded: number, toLoad: number): Promise<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/popular/${currentlyLoaded}/${toLoad}`, { headers: this.headers }).toPromise();
}
public mostWatchedTv(): Observable<ISearchTvResult[]> { public mostWatchedTv(): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/mostwatched`, { headers: this.headers }); return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/mostwatched`, { headers: this.headers });
} }

View file

@ -127,7 +127,7 @@ namespace Ombi.Controllers.V2
/// </summary> /// </summary>
/// <remarks>We use TheMovieDb as the Movie Provider</remarks> /// <remarks>We use TheMovieDb as the Movie Provider</remarks>
/// <returns></returns> /// <returns></returns>
[HttpGet("movie/popular/{currentPostion}/{amountToLoad}")] [HttpGet("movie/popular/{currentPosition}/{amountToLoad}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public async Task<IEnumerable<SearchMovieViewModel>> Popular(int currentPosition, int amountToLoad) public async Task<IEnumerable<SearchMovieViewModel>> Popular(int currentPosition, int amountToLoad)
@ -187,6 +187,19 @@ namespace Ombi.Controllers.V2
return await _tvSearchEngine.Popular(); return await _tvSearchEngine.Popular();
} }
/// <summary>
/// Returns Popular Tv Shows
/// </summary>
/// <remarks>We use Trakt.tv as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/popular/{currentPosition}/{amountToLoad}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<IEnumerable<SearchTvShowViewModel>> PopularTv(int currentPosition, int amountToLoad)
{
return await _tvSearchEngine.Popular(currentPosition, amountToLoad);
}
/// <summary> /// <summary>
/// Returns most Anticiplateds tv shows. /// Returns most Anticiplateds tv shows.
/// </summary> /// </summary>