feat: Recently Requested on Discover Page (#4387)

This commit is contained in:
Jamie 2022-08-09 16:33:55 +01:00 committed by GitHub
parent 26ac75f0c2
commit 44d38fbaae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 3785 additions and 3567 deletions

View file

@ -0,0 +1,205 @@
using AutoFixture;
using MockQueryable.Moq;
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Helpers;
using Ombi.Core.Models.Requests;
using Ombi.Core.Services;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core.Tests.Services
{
[TestFixture]
public class RecentlyRequestedServiceTests
{
private AutoMocker _mocker;
private RecentlyRequestedService _subject;
private Fixture _fixture;
[SetUp]
public void Setup()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mocker = new AutoMocker();
_subject = _mocker.CreateInstance<RecentlyRequestedService>();
}
[Test]
public async Task GetRecentlyRequested_Movies()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings());
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = new List<MovieRequests>
{
new MovieRequests
{
Id = 1,
Approved = true,
Available = true,
ReleaseDate = releaseDate,
Title = "title",
Overview = "overview",
RequestedDate = requestDate,
RequestedUser = new Store.Entities.OmbiUser
{
UserName = "a"
},
RequestedUserId = "b",
}
};
var albums = new List<AlbumRequest>();
var chilRequests = new List<ChildRequests>();
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock().Object);
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Alias = "alias" });
_mocker.Setup<ICurrentUser, string>(x => x.Username).Returns("test");
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First(), Is.InstanceOf<RecentlyRequestedModel>()
.With.Property(nameof(RecentlyRequestedModel.RequestId)).EqualTo(1)
.With.Property(nameof(RecentlyRequestedModel.Approved)).EqualTo(true)
.With.Property(nameof(RecentlyRequestedModel.Available)).EqualTo(true)
.With.Property(nameof(RecentlyRequestedModel.Title)).EqualTo("title")
.With.Property(nameof(RecentlyRequestedModel.Overview)).EqualTo("overview")
.With.Property(nameof(RecentlyRequestedModel.RequestDate)).EqualTo(requestDate)
.With.Property(nameof(RecentlyRequestedModel.ReleaseDate)).EqualTo(releaseDate)
.With.Property(nameof(RecentlyRequestedModel.Type)).EqualTo(RequestType.Movie)
);
}
[Test]
public async Task GetRecentlyRequested_Movies_HideAvailable()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings() { HideAvailableRecentlyRequested = true });
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = new List<MovieRequests>
{
new MovieRequests
{
Id = 1,
Approved = true,
Available = true,
ReleaseDate = releaseDate,
Title = "title",
Overview = "overview",
RequestedDate = requestDate,
RequestedUser = new Store.Entities.OmbiUser
{
UserName = "a"
},
RequestedUserId = "b",
},
new MovieRequests
{
Id = 1,
Approved = true,
Available = false,
ReleaseDate = releaseDate,
Title = "title2",
Overview = "overview2",
RequestedDate = requestDate,
RequestedUser = new Store.Entities.OmbiUser
{
UserName = "a"
},
RequestedUserId = "b",
}
};
var albums = new List<AlbumRequest>();
var chilRequests = new List<ChildRequests>();
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock().Object);
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Alias = "alias" });
_mocker.Setup<ICurrentUser, string>(x => x.Username).Returns("test");
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First(), Is.InstanceOf<RecentlyRequestedModel>()
.With.Property(nameof(RecentlyRequestedModel.RequestId)).EqualTo(1)
.With.Property(nameof(RecentlyRequestedModel.Approved)).EqualTo(true)
.With.Property(nameof(RecentlyRequestedModel.Available)).EqualTo(false)
.With.Property(nameof(RecentlyRequestedModel.Title)).EqualTo("title2")
.With.Property(nameof(RecentlyRequestedModel.Overview)).EqualTo("overview2")
.With.Property(nameof(RecentlyRequestedModel.RequestDate)).EqualTo(requestDate)
.With.Property(nameof(RecentlyRequestedModel.ReleaseDate)).EqualTo(releaseDate)
.With.Property(nameof(RecentlyRequestedModel.Type)).EqualTo(RequestType.Movie)
);
}
[Test]
public async Task GetRecentlyRequested()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings());
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = _fixture.CreateMany<MovieRequests>(10);
var albums = _fixture.CreateMany<AlbumRequest>(10);
var chilRequests = _fixture.CreateMany<ChildRequests>(10);
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock().Object);
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Alias = "alias" });
_mocker.Setup<ICurrentUser, string>(x => x.Username).Returns("test");
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
Assert.That(result.Count, Is.EqualTo(21));
}
[Test]
public async Task GetRecentlyRequested_HideUsernames()
{
_mocker.Setup<ISettingsService<CustomizationSettings>, Task<CustomizationSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new CustomizationSettings());
_mocker.Setup<ISettingsService<OmbiSettings>, Task<OmbiSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new OmbiSettings { HideRequestsUsers = true });
var releaseDate = new DateTime(2019, 01, 01);
var requestDate = DateTime.Now;
var movies = _fixture.CreateMany<MovieRequests>(10);
var albums = _fixture.CreateMany<AlbumRequest>(10);
var chilRequests = _fixture.CreateMany<ChildRequests>(10);
_mocker.Setup<IMovieRequestRepository, IQueryable<MovieRequests>>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock().Object);
_mocker.Setup<IMusicRequestRepository, IQueryable<AlbumRequest>>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock().Object);
_mocker.Setup<ITvRequestRepository, IQueryable<ChildRequests>>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock().Object);
_mocker.Setup<ICurrentUser, Task<OmbiUser>>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Alias = "alias", UserType = UserType.LocalUser });
_mocker.Setup<ICurrentUser, string>(x => x.Username).Returns("test");
_mocker.Setup<OmbiUserManager, Task<bool>>(x => x.IsInRoleAsync(It.IsAny<OmbiUser>(), It.IsAny<string>())).ReturnsAsync(false);
var result = await _subject.GetRecentlyRequested(CancellationToken.None);
CollectionAssert.IsEmpty(result.Where(x => !string.IsNullOrEmpty(x.Username) && !string.IsNullOrEmpty(x.UserId)));
}
}
}

View file

@ -111,11 +111,11 @@ namespace Ombi.Core.Engine
if (model.Is4kRequest)
{
existingRequest.Is4kRequest = true;
existingRequest.RequestedDate4k = DateTime.Now;
existingRequest.RequestedDate4k = DateTime.UtcNow;
}
else
{
existingRequest.RequestedDate = DateTime.Now;
existingRequest.RequestedDate = DateTime.UtcNow;
}
isExisting = true;
requestModel = existingRequest;
@ -134,7 +134,7 @@ namespace Ombi.Core.Engine
? DateTime.Parse(movieInfo.ReleaseDate)
: DateTime.MinValue,
Status = movieInfo.Status,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.Now,
RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.UtcNow,
Approved = false,
Approved4K = false,
RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id,
@ -143,7 +143,7 @@ namespace Ombi.Core.Engine
RequestedByAlias = model.RequestedByAlias,
RootPathOverride = model.RootFolderOverride.GetValueOrDefault(),
QualityOverride = model.QualityPathOverride.GetValueOrDefault(),
RequestedDate4k = model.Is4kRequest ? DateTime.Now : DateTime.MinValue,
RequestedDate4k = model.Is4kRequest ? DateTime.UtcNow : DateTime.MinValue,
Is4kRequest = model.Is4kRequest,
Source = model.Source
};

View file

@ -1,9 +1,12 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core
{
public interface IImageService
{
Task<string> GetTvBackground(string tvdbId);
Task<string> GetTmdbTvBackground(string id, CancellationToken token);
Task<string> GetTmdbTvPoster(string tmdbId, CancellationToken token);
}
}

View file

@ -1,8 +1,13 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Api.FanartTv;
using Ombi.Api.TheMovieDb;
using Ombi.Core.Helpers;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Repository;
namespace Ombi.Core
@ -12,13 +17,19 @@ namespace Ombi.Core
private readonly IApplicationConfigRepository _configRepository;
private readonly IFanartTvApi _fanartTvApi;
private readonly ICacheService _cache;
private readonly IMovieDbApi _movieDbApi;
private readonly ICurrentUser _user;
private readonly ISettingsService<OmbiSettings> _ombiSettings;
public ImageService(IApplicationConfigRepository configRepository, IFanartTvApi fanartTvApi,
ICacheService cache)
ICacheService cache, IMovieDbApi movieDbApi, ICurrentUser user, ISettingsService<OmbiSettings> ombiSettings)
{
_configRepository = configRepository;
_fanartTvApi = fanartTvApi;
_cache = cache;
_movieDbApi = movieDbApi;
_user = user;
_ombiSettings = ombiSettings;
}
public async Task<string> GetTvBackground(string tvdbId)
@ -43,5 +54,69 @@ namespace Ombi.Core
return string.Empty;
}
public async Task<string> GetTmdbTvBackground(string id, CancellationToken token)
{
var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}tv{id}", () => _movieDbApi.GetTvImages(id, token), DateTimeOffset.Now.AddDays(1));
if (images?.backdrops?.Any() ?? false)
{
return images.backdrops.Select(x => x.file_path).FirstOrDefault();
}
if (images?.posters?.Any() ?? false)
{
return images.posters.Select(x => x.file_path).FirstOrDefault();
}
return string.Empty;
}
public async Task<string> GetTmdbTvPoster(string tmdbId, CancellationToken token)
{
var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}tv{tmdbId}", () => _movieDbApi.GetTvImages(tmdbId, token), DateTimeOffset.Now.AddDays(1));
if (images?.posters?.Any() ?? false)
{
var lang = await DefaultLanguageCode();
var langImage = images.posters.Where(x => lang.Equals(x.iso_639_1, StringComparison.InvariantCultureIgnoreCase)).OrderByDescending(x => x.vote_count);
if (langImage.Any())
{
return langImage.Select(x => x.file_path).First();
}
else
{
return images.posters.Select(x => x.file_path).First();
}
}
if (images?.backdrops?.Any() ?? false)
{
return images.backdrops.Select(x => x.file_path).FirstOrDefault();
}
return string.Empty;
}
protected async Task<string> DefaultLanguageCode()
{
var user = await _user.GetUser();
if (user == null)
{
return "en";
}
if (string.IsNullOrEmpty(user.Language))
{
var s = await GetOmbiSettings();
return s.DefaultLanguageCode;
}
return user.Language;
}
private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings()
{
return ombiSettings ?? (ombiSettings = await _ombiSettings.GetSettingsAsync());
}
}
}

View file

@ -0,0 +1,21 @@
using Ombi.Store.Entities;
using System;
namespace Ombi.Core.Models.Requests
{
public class RecentlyRequestedModel
{
public int RequestId { get; set; }
public RequestType Type { get; set; }
public string UserId { get; set; }
public string Username { get; set; }
public bool Available { get; set; }
public bool TvPartiallyAvailable { get; set; }
public DateTime RequestDate { get; set; }
public string Title { get; set; }
public string Overview { get; set; }
public DateTime ReleaseDate { get; set; }
public bool Approved { get; set; }
public string MediaId { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using Ombi.Core.Models.Requests;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Core.Services
{
public interface IRecentlyRequestedService
{
Task<IEnumerable<RecentlyRequestedModel>> GetRecentlyRequested(CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,146 @@
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Helpers;
using Ombi.Core.Models.Requests;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static Ombi.Core.Engine.BaseMediaEngine;
namespace Ombi.Core.Services
{
public class RecentlyRequestedService : BaseEngine, IRecentlyRequestedService
{
private readonly IMovieRequestRepository _movieRequestRepository;
private readonly ITvRequestRepository _tvRequestRepository;
private readonly IMusicRequestRepository _musicRequestRepository;
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly ISettingsService<OmbiSettings> _ombiSettings;
private const int AmountToTake = 7;
public RecentlyRequestedService(
IMovieRequestRepository movieRequestRepository,
ITvRequestRepository tvRequestRepository,
IMusicRequestRepository musicRequestRepository,
ISettingsService<CustomizationSettings> customizationSettings,
ISettingsService<OmbiSettings> ombiSettings,
ICurrentUser user,
OmbiUserManager um,
IRuleEvaluator rules) : base(user, um, rules)
{
_movieRequestRepository = movieRequestRepository;
_tvRequestRepository = tvRequestRepository;
_musicRequestRepository = musicRequestRepository;
_customizationSettings = customizationSettings;
_ombiSettings = ombiSettings;
}
public async Task<IEnumerable<RecentlyRequestedModel>> GetRecentlyRequested(CancellationToken cancellationToken)
{
var customizationSettingsTask = _customizationSettings.GetSettingsAsync();
var recentMovieRequests = _movieRequestRepository.GetAll().Include(x => x.RequestedUser).OrderByDescending(x => x.RequestedDate).Take(AmountToTake);
var recentTvRequests = _tvRequestRepository.GetChild().Include(x => x.RequestedUser).Include(x => x.ParentRequest).OrderByDescending(x => x.RequestedDate).Take(AmountToTake);
var recentMusicRequests = _musicRequestRepository.GetAll().Include(x => x.RequestedUser).OrderByDescending(x => x.RequestedDate).Take(AmountToTake);
var settings = await customizationSettingsTask;
if (settings.HideAvailableRecentlyRequested)
{
recentMovieRequests = recentMovieRequests.Where(x => !x.Available);
recentTvRequests = recentTvRequests.Where(x => !x.Available);
recentMusicRequests = recentMusicRequests.Where(x => !x.Available);
}
var hideUsers = await HideFromOtherUsers();
var model = new List<RecentlyRequestedModel>();
foreach (var item in await recentMovieRequests.ToListAsync(cancellationToken))
{
model.Add(new RecentlyRequestedModel
{
RequestId = item.Id,
Available = item.Available,
Overview = item.Overview,
ReleaseDate = item.ReleaseDate,
RequestDate = item.RequestedDate,
Title = item.Title,
Type = RequestType.Movie,
Approved = item.Approved,
UserId = hideUsers.Hide ? string.Empty : item.RequestedUserId,
Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias,
MediaId = item.TheMovieDbId.ToString(),
});
}
foreach (var item in await recentMusicRequests.ToListAsync(cancellationToken))
{
model.Add(new RecentlyRequestedModel
{
RequestId = item.Id,
Available = item.Available,
Overview = item.ArtistName,
Approved = item.Approved,
ReleaseDate = item.ReleaseDate,
RequestDate = item.RequestedDate,
Title = item.Title,
Type = RequestType.Album,
UserId = hideUsers.Hide ? string.Empty : item.RequestedUserId,
Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias,
MediaId = item.ForeignAlbumId,
});
}
foreach (var item in await recentTvRequests.ToListAsync(cancellationToken))
{
var partialAvailability = item.SeasonRequests.SelectMany(x => x.Episodes).Any(e => e.Available);
model.Add(new RecentlyRequestedModel
{
RequestId = item.Id,
Available = item.Available,
Overview = item.ParentRequest.Overview,
ReleaseDate = item.ParentRequest.ReleaseDate,
Approved = item.Approved,
RequestDate = item.RequestedDate,
TvPartiallyAvailable = partialAvailability,
Title = item.ParentRequest.Title,
Type = RequestType.TvShow,
UserId = hideUsers.Hide ? string.Empty : item.RequestedUserId,
Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias,
MediaId = item.ParentRequest.ExternalProviderId.ToString()
});
}
return model.OrderByDescending(x => x.RequestDate);
}
private async Task<HideResult> HideFromOtherUsers()
{
var user = await GetUser();
if (await IsInRole(OmbiRoles.Admin) || await IsInRole(OmbiRoles.PowerUser) || user.IsSystemUser)
{
return new HideResult
{
UserId = user.Id
};
}
var settings = await _ombiSettings.GetSettingsAsync();
var result = new HideResult
{
Hide = settings.HideRequestsUsers,
UserId = user.Id
};
return result;
}
}
}

View file

@ -228,6 +228,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILegacyMobileNotification, LegacyMobileNotification>();
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();
services.AddScoped<IFeatureService, FeatureService>();
services.AddTransient<IRecentlyRequestedService, RecentlyRequestedService>();
}
public static void RegisterJobs(this IServiceCollection services)

View file

@ -21,6 +21,7 @@ namespace Ombi.Helpers
public const string LidarrRootFolders = nameof(LidarrRootFolders);
public const string LidarrQualityProfiles = nameof(LidarrQualityProfiles);
public const string FanartTv = nameof(FanartTv);
public const string TmdbImages = nameof(TmdbImages);
public const string UsersDropdown = nameof(UsersDropdown);
}
}

View file

@ -13,6 +13,7 @@
public bool UseCustomPage { get; set; }
public bool HideAvailableFromDiscover { get; set; }
public string Favicon { get; set; }
public bool HideAvailableRecentlyRequested { get; set; }
public string AddToUrl(string part)
{

View file

@ -46,5 +46,6 @@ namespace Ombi.Api.TheMovieDb
Task<List<Language>> GetLanguages(CancellationToken cancellationToken);
Task<List<WatchProvidersResults>> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken);
Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, int page, CancellationToken cancellationToken);
Task<TvImages> GetTvImages(string theMovieDbId, CancellationToken token);
}
}

View file

@ -0,0 +1,44 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class TvImages
{
public Backdrop[] backdrops { get; set; }
public int id { get; set; }
public Logo[] logos { get; set; }
public Poster[] posters { get; set; }
}
public class Backdrop
{
public float aspect_ratio { get; set; }
public int height { get; set; }
public string iso_639_1 { get; set; }
public string file_path { get; set; }
public float vote_average { get; set; }
public int vote_count { get; set; }
public int width { get; set; }
}
public class Logo
{
public float aspect_ratio { get; set; }
public int height { get; set; }
public string iso_639_1 { get; set; }
public string file_path { get; set; }
public float vote_average { get; set; }
public int vote_count { get; set; }
public int width { get; set; }
}
public class Poster
{
public float aspect_ratio { get; set; }
public int height { get; set; }
public string iso_639_1 { get; set; }
public string file_path { get; set; }
public float vote_average { get; set; }
public int vote_count { get; set; }
public int width { get; set; }
}
}

View file

@ -514,6 +514,14 @@ namespace Ombi.Api.TheMovieDb
return Api.Request<WatchProviders>(request, token);
}
public Task<TvImages> GetTvImages(string theMovieDbId, CancellationToken token)
{
var request = new Request($"tv/{theMovieDbId}/images", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
return Api.Request<TvImages>(request, token);
}
private async Task AddDiscoverSettings(Request request)
{
var settings = await Settings;

View file

@ -6,13 +6,14 @@ module.exports = {
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
"@storybook/addon-interactions",
"@storybook/preset-scss",
],
"framework": "@storybook/angular",
"core": {
"builder": "@storybook/builder-webpack5"
},
"staticDirs": [{from :'../../wwwroot/images', to: 'images'}],
"staticDirs": [{ from: '../../wwwroot/images', to: 'images' }, { from: '../../wwwroot/translations', to: 'translations'}],
"features": {
interactionsDebugger: true,
}

View file

@ -5,5 +5,6 @@
body {
background: #0f171f;
color: white;
}
</style>

View file

@ -1,5 +1,8 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
import '../src/styles/_imports.scss';
setCompodocJson(docJson);
export const parameters = {

View file

@ -81,9 +81,9 @@
"@storybook/addon-links": "^6.5.9",
"@storybook/angular": "^6.5.9",
"@storybook/builder-webpack5": "^6.5.9",
"@storybook/jest": "^0.0.10",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"@storybook/preset-scss": "^1.0.3",
"@types/jasmine": "~3.6.7",
"@types/jasminewd2": "~2.0.8",
"@types/node": "^16.11.45",

View file

@ -0,0 +1,18 @@
.detailed-container {
width: 400px;
::ng-deep .poster {
border-radius: 10px;
opacity: 1;
display: block;
width: 100%;
height: auto;
transition: .5s ease;
backface-visibility: hidden;
}
}

View file

@ -0,0 +1,41 @@
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { APP_BASE_HREF } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { Story, Meta, moduleMetadata } from '@storybook/angular';
import { ButtonComponent } from './button.component';
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
title: 'Button Component',
component: ButtonComponent,
decorators: [
moduleMetadata({
providers: [
{
provide: APP_BASE_HREF,
useValue: {}
},
MatButtonModule
]
})
]
} as Meta;
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
props: args,
});
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
Primary.args = {
type: 'primary',
text: 'Primary',
};
export const Secondary = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
Secondary.args = {
type: 'accent',
text: 'Secondary',
};

View file

@ -0,0 +1,25 @@
import { OmbiCommonModules } from "../modules";
import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
import { MatButtonModule } from "@angular/material/button";
@Component({
standalone: true,
selector: 'ombi-button',
imports: [...OmbiCommonModules, MatButtonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./button.component.scss'],
template: `
<button [id]="id" [type]="type" [class]="class" [data-toggle]="dataToggle" mat-raised-button [data-target]="dataTarget">{{text}}</button>
`
})
export class ButtonComponent {
@Input() public text: string;
@Input() public id: string;
@Input() public type: string;
@Input() public class: string;
@Input('data-toggle') public dataToggle: string;
@Input('data-target') public dataTarget: string;
}

View file

@ -0,0 +1,25 @@
<div id="detailed-{{request.mediaId}}" class="detailed-container" (click)="click()" [style.background-image]="background">
<div class="row">
<div class="col-xl-5 col-lg-5 col-md-5 col-sm-12 posterColumn">
<ombi-image [src]="request.posterPath" [type]="request.type" class="poster" alt="{{request.title}}">
</ombi-image>
</div>
<div class="col-xl-7 col-lg-7 col-md-7 col-sm-12">
<div class="row">
<div class="col-12 title">
<div class="text-right year"><sup>{{request.releaseDate | date:'yyyy'}}</sup></div>
<h3 id="detailed-request-title-{{request.mediaId}}">{{request.title}}</h3>
</div>
<div class="col-12" *ngIf="request.username">
<p id="detailed-request-requestedby-{{request.mediaId}}">Requested By: {{request.username}}</p>
</div>
<div class="col-12">
<p id="detailed-request-date-{{request.mediaId}}">On: {{request.requestDate | amFromUtc | amLocal | amUserLocale | amDateFormat: 'l LT'}}</p>
</div>
<div class="col-12">
<p id="detailed-request-status-{{request.mediaId}}">Status: <span class="badge badge-{{getClass(request)}}">{{getStatus(request) | translate}}</span></p>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,47 @@
@import "~styles/variables.scss";
.detailed-container {
width: 400px;
height: auto;
margin: 10px;
padding: 10px;
border-radius: 10px;
border: 1px solid $ombi-background-primary-accent;
@media (max-width:768px) {
width: 200px;
}
background-color: $ombi-background-accent;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
.overview {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.year {
@media (max-width:768px) {
padding-top: 3%;
}
}
::ng-deep .poster {
cursor: pointer;
border-radius: 10px;
opacity: 1;
display: block;
width: 100%;
height: 200px;
transition: .5s ease;
backface-visibility: hidden;
border: 1px solid #35465c;
}
}

View file

@ -0,0 +1,209 @@
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { Story, Meta, moduleMetadata } from '@storybook/angular';
import { IRecentlyRequested, RequestType } from '../../interfaces';
import { DetailedCardComponent } from './detailed-card.component';
import { TranslateModule } from "@ngx-translate/core";
import { ImageService } from "../../services/image.service";
import { Observable, of } from 'rxjs';
import { SharedModule } from '../../shared/shared.module';
import { PipeModule } from '../../pipes/pipe.module';
import { ImageComponent } from '../image/image.component';
function imageServiceMock(): Partial<ImageService> {
return {
getMoviePoster: () : Observable<string> => of("https://assets.fanart.tv/fanart/movies/603/movieposter/the-matrix-52256ae1021be.jpg"),
getMovieBackground : () : Observable<string> => of("https://assets.fanart.tv/fanart/movies/603/movieposter/the-matrix-52256ae1021be.jpg"),
getTmdbTvPoster : () : Observable<string> => of("/bfxwMdQyJc0CL24m5VjtWAN30mt.jpg"),
getTmdbTvBackground : () : Observable<string> => of("/bfxwMdQyJc0CL24m5VjtWAN30mt.jpg"),
};
}
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
title: 'Detailed Card Component',
component: DetailedCardComponent,
decorators: [
moduleMetadata({
providers: [
{
provide: APP_BASE_HREF,
useValue: {}
},
{
provide: ImageService,
useValue: imageServiceMock()
}
],
imports: [
TranslateModule.forRoot(),
CommonModule,
ImageComponent,
SharedModule,
PipeModule
]
})
]
} as Meta;
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<DetailedCardComponent> = (args: DetailedCardComponent) => ({
props: args,
});
export const NewMovieRequest = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
NewMovieRequest.args = {
request: {
title: 'The Matrix',
approved: false,
available: false,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.movie,
mediaId: '603',
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const MovieNoUsername = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
MovieNoUsername.args = {
request: {
title: 'The Matrix',
approved: false,
available: false,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
userId: '12345',
type: RequestType.movie,
mediaId: '603',
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const AvailableMovie = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
AvailableMovie.args = {
request: {
title: 'The Matrix',
approved: false,
available: true,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.movie,
mediaId: '603',
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const ApprovedMovie = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
ApprovedMovie.args = {
request: {
title: 'The Matrix',
approved: true,
available: false,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.movie,
mediaId: '603',
overview: 'The Matrix is a movie about a group of people who are forced to fight against a powerful computer system that controls them.',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const NewTvRequest = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
NewTvRequest.args = {
request: {
title: 'For All Mankind',
approved: false,
available: false,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.tvShow,
mediaId: '603',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const ApprovedTv = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
ApprovedTv.args = {
request: {
title: 'For All Mankind',
approved: true,
available: false,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.tvShow,
mediaId: '603',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const AvailableTv = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
AvailableTv.args = {
request: {
title: 'For All Mankind',
approved: true,
available: true,
tvPartiallyAvailable: false,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.tvShow,
mediaId: '603',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const PartiallyAvailableTv = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
PartiallyAvailableTv.args = {
request: {
title: 'For All Mankind',
approved: true,
available: false,
tvPartiallyAvailable: true,
requestDate: new Date(2022, 1, 1),
username: 'John Doe',
userId: '12345',
type: RequestType.tvShow,
mediaId: '603',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};
export const TvNoUsername = Template.bind({});
// More on args: https://storybook.js.org/docs/angular/writing-stories/args
TvNoUsername.args = {
request: {
title: 'For All Mankind',
approved: true,
available: false,
tvPartiallyAvailable: true,
requestDate: new Date(2022, 1, 1),
userId: '12345',
type: RequestType.tvShow,
mediaId: '603',
releaseDate: new Date(2020, 1, 1),
} as IRecentlyRequested,
};

View file

@ -0,0 +1,81 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { IRecentlyRequested, RequestType } from "../../interfaces";
import { ImageService } from "app/services";
import { Subject, takeUntil } from "rxjs";
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
@Component({
standalone: false,
selector: 'ombi-detailed-card',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './detailed-card.component.html',
styleUrls: ['./detailed-card.component.scss']
})
export class DetailedCardComponent implements OnInit, OnDestroy {
@Input() public request: IRecentlyRequested;
@Output() public onClick: EventEmitter<void> = new EventEmitter<void>();
public RequestType = RequestType;
public loading: false;
private $imageSub = new Subject<void>();
public background: SafeStyle;
constructor(private imageService: ImageService, private sanitizer: DomSanitizer) { }
ngOnInit(): void {
if (!this.request.posterPath) {
switch (this.request.type) {
case RequestType.movie:
this.imageService.getMoviePoster(this.request.mediaId).pipe(takeUntil(this.$imageSub)).subscribe(x => this.request.posterPath = x);
this.imageService.getMovieBackground(this.request.mediaId).pipe(takeUntil(this.$imageSub)).subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(" + x + ")");
});
break;
case RequestType.tvShow:
this.imageService.getTmdbTvPoster(Number(this.request.mediaId)).pipe(takeUntil(this.$imageSub)).subscribe(x => this.request.posterPath = `https://image.tmdb.org/t/p/w300${x}`);
this.imageService.getTmdbTvBackground(Number(this.request.mediaId)).pipe(takeUntil(this.$imageSub)).subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(https://image.tmdb.org/t/p/w300" + x + ")");
});
break;
}
}
}
public getStatus(request: IRecentlyRequested) {
if (request.available) {
return "Common.Available";
}
if (request.tvPartiallyAvailable) {
return "Common.PartiallyAvailable";
}
if (request.approved) {
return "Common.Approved";
} else {
return "Common.Pending";
}
}
public click() {
this.onClick.emit();
}
public getClass(request: IRecentlyRequested) {
if (request.available || request.tvPartiallyAvailable) {
return "success";
}
if (request.approved) {
return "primary";
} else {
return "info";
}
}
public ngOnDestroy() {
this.$imageSub.next();
this.$imageSub.complete();
}
}

View file

@ -28,6 +28,8 @@ import { APP_BASE_HREF } from "@angular/common";
private defaultMovie = "/images/default_movie_poster.png";
private defaultMusic = "i/mages/default-music-placeholder.png";
private alreadyErrored = false;
constructor (@Inject(APP_BASE_HREF) public href: string) {
if (this.href.length > 1) {
this.baseUrl = this.href;
@ -35,6 +37,9 @@ import { APP_BASE_HREF } from "@angular/common";
}
public onError(event: any) {
if (this.alreadyErrored) {
return;
}
// set to a placeholder
switch(this.type) {
case RequestType.movie:
@ -48,10 +53,11 @@ import { APP_BASE_HREF } from "@angular/common";
break;
}
this.alreadyErrored = true;
// Retry the original image
const timeout = setTimeout(() => {
event.target.src = this.src;
clearTimeout(timeout);
event.target.src = this.src;
}, Math.floor(Math.random() * (7000 - 1000 + 1)) + 1000);
}
}

View file

@ -1,2 +1,3 @@
export * from "./image-background/image-background.component";
export * from "./image/image.component";
export * from "./image/image.component";
export * from "./detailed-card/detailed-card.component";

View file

@ -1,3 +1,4 @@
import { CommonModule } from "@angular/common";
import { MomentModule } from "ngx-moment";
export const OmbiCommonModules = [ CommonModule ];
export const OmbiCommonModules = [ CommonModule, MomentModule ];

View file

@ -0,0 +1,37 @@
export const ResponsiveOptions = [
{
breakpoint: '1800px',
numVisible: 5,
numScroll: 4
},
{
breakpoint: '1650px',
numVisible: 3,
numScroll: 1
},
{
breakpoint: '1500px',
numVisible: 3,
numScroll: 3
},
{
breakpoint: '900px',
numVisible: 1,
numScroll: 1
},
{
breakpoint: '768px',
numVisible: 2,
numScroll: 2
},
{
breakpoint: '660px',
numVisible: 1,
numScroll: 1
},
{
breakpoint: '480px',
numVisible: 1,
numScroll: 1
}
];

View file

@ -1,5 +1,12 @@
<div class="small-middle-container">
<div class="section">
<h2>{{'Discovery.RecentlyRequestedTab' | translate}}</h2>
<div>
<ombi-recently-list [id]="'recentlyRequested'"></ombi-recently-list>
</div>
</div>
<div class="section" [hidden]="!showSeasonal">
<h2>{{'Discovery.SeasonalTab' | translate}}</h2>
<div>
@ -29,10 +36,5 @@
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
</div>
</div>
<!-- <div class="section">
<h2>{{'Discovery.RecentlyRequestedTab' | translate}}</h2>
<div>
<carousel-list [id]="'recentlyRequested'" [discoverType]="DiscoverType.RecentlyRequested"></carousel-list>
</div>
</div> -->
</div>

View file

@ -7,9 +7,11 @@ import { DiscoverCardComponent } from "./card/discover-card.component";
import { DiscoverCollectionsComponent } from "./collections/discover-collections.component";
import { DiscoverComponent } from "./discover/discover.component";
import { DiscoverSearchResultsComponent } from "./search-results/search-results.component";
import { RecentlyRequestedListComponent } from "./recently-requested-list/recently-requested-list.component";
import { MatDialog } from "@angular/material/dialog";
import { RequestServiceV2 } from "../../services/requestV2.service";
import { Routes } from "@angular/router";
import { DetailedCardComponent } from "app/components";
export const components: any[] = [
DiscoverComponent,
@ -18,6 +20,8 @@ export const components: any[] = [
DiscoverActorComponent,
DiscoverSearchResultsComponent,
CarouselListComponent,
RecentlyRequestedListComponent,
DetailedCardComponent,
];
export const providers: any[] = [

View file

@ -0,0 +1,5 @@
<p-carousel #carousel [value]="requests" [numVisible]="3" [numScroll]="1" [responsiveOptions]="responsiveOptions" [page]="0">
<ng-template let-result pTemplate="item">
<ombi-detailed-card [request]="result" (onClick)="navigate(result)"></ombi-detailed-card>
</ng-template>
</p-carousel>

View file

@ -0,0 +1,112 @@
@import "~styles/variables.scss";
.ombi-card {
padding: 5px;
}
::ng-deep .p-carousel-indicators {
display: none !important;
}
.image {
border-radius: 10px;
opacity: 1;
display: block;
width: 100%;
height: auto;
transition: .5s ease;
backface-visibility: hidden;
}
.middle {
transition: .5s ease;
opacity: 0;
position: absolute;
top: 75%;
width: 90%;
left: 50%;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
}
.c {
position: relative;
}
.c:hover .image {
opacity: 0.3;
}
.c:hover .middle {
opacity: 1;
}
.small-text {
font-size: 11px;
}
.title {
font-size: 16px;
}
.top-left {
font-size: 14px;
position: absolute;
top: 8px;
left: 16px;
}
.right {
text-align: right;
margin-top:-61px;
}
@media (max-width:520px){
.right{
margin-top:0px;
text-align: center;;
}
}
.discover-filter-buttons-group {
background: $ombi-background-primary;
border: 1px solid #293a4c;
border-radius: 30px;
color:#fff;
margin-bottom:10px;
margin-right: 30px;
.discover-filter-button{
background:inherit;
color:inherit;
padding:0 0px;
border-radius: 30px;
padding-left: 20px;
padding-right: 20px;
border-left:none;
}
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content{
line-height:40px;
}
.button-active{
background:#293a4c;
}
}
::ng-deep .discover-filter-button .mat-button-toggle-button:focus{
outline:none;
}
.card-skeleton {
padding: 5px;
}
@media (min-width:755px){
::ng-deep .p-carousel-item{
flex: 1 0 200px !important;
}
}

View file

@ -0,0 +1,91 @@
import { Component, OnInit, Input, ViewChild, Output, EventEmitter, OnDestroy } from "@angular/core";
import { DiscoverOption, IDiscoverCardResult } from "../../interfaces";
import { IRecentlyRequested, ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces";
import { SearchV2Service } from "../../../services";
import { StorageService } from "../../../shared/storage/storage-service";
import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { Carousel } from 'primeng/carousel';
import { FeaturesFacade } from "../../../state/features/features.facade";
import { ResponsiveOptions } from "../carousel.options";
import { RequestServiceV2 } from "app/services/requestV2.service";
import { Subject, takeUntil } from "rxjs";
import { Router } from "@angular/router";
export enum DiscoverType {
Upcoming,
Trending,
Popular,
RecentlyRequested,
Seasonal,
}
@Component({
selector: "ombi-recently-list",
templateUrl: "./recently-requested-list.component.html",
styleUrls: ["./recently-requested-list.component.scss"],
})
export class RecentlyRequestedListComponent implements OnInit, OnDestroy {
@Input() public id: string;
@Input() public isAdmin: boolean;
@ViewChild('carousel', {static: false}) carousel: Carousel;
public requests: IRecentlyRequested[];
public responsiveOptions: any;
public RequestType = RequestType;
public loadingFlag: boolean;
public DiscoverType = DiscoverType;
public is4kEnabled = false;
private $loadSub = new Subject<void>();
constructor(private requestService: RequestServiceV2,
private featureFacade: FeaturesFacade,
private router: Router) {
Carousel.prototype.onTouchMove = () => {},
this.responsiveOptions = ResponsiveOptions;
}
ngOnDestroy(): void {
this.$loadSub.next();
this.$loadSub.complete();
}
public ngOnInit() {
this.loading();
this.loadData();
}
public navigate(request: IRecentlyRequested) {
this.router.navigate([this.generateDetailsLink(request), request.mediaId]);
}
private generateDetailsLink(request: IRecentlyRequested): string {
switch (request.type) {
case RequestType.movie:
return `/details/movie/`;
case RequestType.tvShow:
return `/details/tv/`;
case RequestType.album: //Actually artist
return `/details/artist/`;
}
}
private loadData() {
this.requestService.getRecentlyRequested().pipe(takeUntil(this.$loadSub)).subscribe(x => {
this.requests = x;
this.finishLoading();
});
}
private loading() {
this.loadingFlag = true;
}
private finishLoading() {
this.loadingFlag = false;
}
}

View file

@ -19,7 +19,7 @@ import { ImageComponent } from 'app/components';
MatButtonToggleModule,
InfiniteScrollModule,
SkeletonModule,
ImageComponent
ImageComponent,
],
declarations: [
...fromComponents.components

View file

@ -0,0 +1,18 @@
import { RequestType } from "./IRequestModel";
export interface IRecentlyRequested {
requestId: number;
userId: string;
username: string;
available: boolean;
tvPartiallyAvailable: boolean;
requestDate: Date;
title: string;
overview: string;
releaseDate: Date;
approved: boolean;
mediaId: string;
type: RequestType;
posterPath: string;
}

View file

@ -21,3 +21,4 @@ export * from "./IVote";
export * from "./IFailedRequests";
export * from "./IHub";
export * from "./ITester";
export * from "./IRecentlyRequested";

View file

@ -4,8 +4,6 @@ import { Story, Meta, moduleMetadata } from '@storybook/angular';
import { SocialIconsComponent } from './social-icons.component';
import { MatMenuModule } from "@angular/material/menu";
import { RequestType } from '../../../../interfaces';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {

View file

@ -33,6 +33,10 @@ export class ImageService extends ServiceHelpers {
return this.http.get<string>(`${this.url}poster/tv/${tvdbid}`, { headers: this.headers });
}
public getTmdbTvPoster(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}poster/tv/tmdb/${tvdbid}`, { headers: this.headers });
}
public getMovieBackground(movieDbId: string): Observable<string> {
return this.http.get<string>(`${this.url}background/movie/${movieDbId}`, { headers: this.headers });
}
@ -45,4 +49,7 @@ export class ImageService extends ServiceHelpers {
return this.http.get<string>(`${this.url}background/tv/${tvdbid}`, { headers: this.headers });
}
public getTmdbTvBackground(id: number): Observable<string> {
return this.http.get<string>(`${this.url}background/tv/tmdb/${id}`, { headers: this.headers });
}
}

View file

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { ServiceHelpers } from "./service.helpers";
import { IRequestsViewModel, IMovieRequests, IChildRequests, IMovieAdvancedOptions as IMediaAdvancedOptions, IRequestEngineResult, IAlbumRequest, ITvRequestViewModelV2, RequestType } from "../interfaces";
import { IRequestsViewModel, IMovieRequests, IChildRequests, IMovieAdvancedOptions as IMediaAdvancedOptions, IRequestEngineResult, IAlbumRequest, ITvRequestViewModelV2, RequestType, IRecentlyRequested } from "../interfaces";
@Injectable()
@ -100,4 +100,8 @@ export class RequestServiceV2 extends ServiceHelpers {
public requestMovieCollection(collectionId: number): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}movie/collection/${collectionId}`, undefined, { headers: this.headers });
}
public getRecentlyRequested(): Observable<IRecentlyRequested[]> {
return this.http.get<IRecentlyRequested[]>(`${this.url}recentlyRequested`, { headers: this.headers });
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Ombi.Api.FanartTv;
using Ombi.Api.TheMovieDb;
using Ombi.Config;
using Ombi.Core;
using Ombi.Core.Engine.Interfaces;
@ -17,11 +19,12 @@ namespace Ombi.Controllers.V1
[ApiController]
public class ImagesController : ControllerBase
{
public ImagesController(IFanartTvApi fanartTvApi, IApplicationConfigRepository config,
public ImagesController(IFanartTvApi fanartTvApi, IMovieDbApi movieDbApi, IApplicationConfigRepository config,
IOptions<LandingPageBackground> options, ICacheService c, IImageService imageService,
IMovieEngineV2 movieEngineV2, ITVSearchEngineV2 tVSearchEngineV2)
{
FanartTvApi = fanartTvApi;
_movieDbApi = movieDbApi;
Config = config;
Options = options.Value;
_cache = c;
@ -33,6 +36,8 @@ namespace Ombi.Controllers.V1
private IFanartTvApi FanartTvApi { get; }
private IApplicationConfigRepository Config { get; }
private LandingPageBackground Options { get; }
private readonly IMovieDbApi _movieDbApi;
private readonly ICacheService _cache;
private readonly IImageService _imageService;
private readonly IMovieEngineV2 _movieEngineV2;
@ -175,6 +180,10 @@ namespace Ombi.Controllers.V1
return string.Empty;
}
[HttpGet("poster/tv/tmdb/{tmdbId}")]
public Task<string> GetTmdbTvPoster(string tmdbId) => _imageService.GetTmdbTvPoster(tmdbId, HttpContext.RequestAborted);
[HttpGet("background/movie/{movieDbId}")]
public async Task<string> GetMovieBackground(string movieDbId)
{
@ -236,6 +245,10 @@ namespace Ombi.Controllers.V1
return await _imageService.GetTvBackground(tvdbid.ToString());
}
[HttpGet("background/tv/tmdb/{id}")]
public Task<string> GetTmdbTvBackground(string id) => _imageService.GetTmdbTvBackground(id, HttpContext.RequestAborted);
[HttpGet("background")]
public async Task<object> GetBackgroundImage()
{

View file

@ -13,6 +13,8 @@ using System.Linq;
using Microsoft.Extensions.Logging;
using Ombi.Attributes;
using Ombi.Helpers;
using Ombi.Core.Services;
using System.Collections.Generic;
namespace Ombi.Controllers.V2
{
@ -24,15 +26,17 @@ namespace Ombi.Controllers.V2
private readonly IMusicRequestEngine _musicRequestEngine;
private readonly IVoteEngine _voteEngine;
private readonly ILogger<RequestsController> _logger;
private readonly IRecentlyRequestedService _recentlyRequestedService;
public RequestsController(IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, IMusicRequestEngine musicRequestEngine,
IVoteEngine voteEngine, ILogger<RequestsController> logger)
IVoteEngine voteEngine, ILogger<RequestsController> logger, IRecentlyRequestedService recentlyRequestedService)
{
_movieRequestEngine = movieRequestEngine;
_tvRequestEngine = tvRequestEngine;
_musicRequestEngine = musicRequestEngine;
_voteEngine = voteEngine;
_logger = logger;
_recentlyRequestedService = recentlyRequestedService;
}
/// <summary>
@ -223,6 +227,12 @@ namespace Ombi.Controllers.V2
return await _movieRequestEngine.RequestCollection(collectionId, HttpContext.RequestAborted);
}
[HttpGet("recentlyRequested")]
public Task<IEnumerable<RecentlyRequestedModel>> RecentlyRequested()
{
return _recentlyRequestedService.GetRecentlyRequested(CancellationToken);
}
private string GetApiAlias()
{
// Make sure this only applies when using the API KEY

View file

@ -26,8 +26,35 @@ class CarouselComponent {
}
}
class RecentlyRequestedComponent {
getRequest(id: string): DetailedCard {
return new DetailedCard(id);
}
}
class DetailedCard {
private id: string;
get title(): Cypress.Chainable<any> {
return cy.get(`#detailed-request-title-${this.id}`);
}
get status(): Cypress.Chainable<any> {
return cy.get(`#detailed-request-status-${this.id}`);
}
verifyTitle(expected: string): Cypress.Chainable<any> {
return this.title.should('have.text',expected);
}
constructor(id: string) {
this.id = id;
}
}
class DiscoverPage extends BasePage {
popularCarousel = new CarouselComponent("popular");
recentlyRequested = new RecentlyRequestedComponent();
adminOptionsDialog = new AdminRequestDialog();
constructor() {

View file

@ -0,0 +1,164 @@
import { discoverPage as Page } from "@/integration/page-objects";
describe("Discover Recently Requested Tests", () => {
beforeEach(() => {
cy.login();
});
it("Requested Movie Is Displayed", () => {
cy.requestMovie(315635);
cy.intercept("GET", "**/v2/Requests/recentlyRequested").as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("315635");
card.verifyTitle("Spider-Man: Homecoming");
card.status.should('contain.text', 'Approved');
});
});
it("Requested Movie Is Pending Approval", () => {
cy.requestMovie(626735);
cy.intercept("GET", "**/v2/Requests/recentlyRequested", (req) => {
req.reply((res) => {
const body = res.body;
const movie = body[0];
movie.available = false;
movie.approved = false;
body[0] = movie;
res.send(body);
});
}).as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("626735");
card.verifyTitle("Dog");
card.status.should('contain.text', 'Pending');
});
});
it("Requested Movie Is Available", () => {
cy.requestMovie(675353);
cy.intercept("GET", "**/v2/Requests/recentlyRequested", (req) => {
req.reply((res) => {
const body = res.body;
const movie = body[0];
movie.available = true;
body[0] = movie;
res.send(body);
});
}).as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("675353");
card.verifyTitle("Sonic the Hedgehog 2");
card.status.should('contain.text', 'Available'); // Because admin auto request
});
});
it("Requested TV Is Available", () => {
cy.requestAllTv(135647);
cy.intercept("GET", "**/v2/Requests/recentlyRequested", (req) => {
req.reply((res) => {
const body = res.body;
const tv = body[0];
tv.available = true;
body[0] = tv;
res.send(body);
});
}).as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("135647");
card.verifyTitle("2 Good 2 Be True");
card.status.should('contain.text', 'Available');
});
});
it("Requested TV Is Partially Available", () => {
cy.requestAllTv(158415);
cy.intercept("GET", "**/v2/Requests/recentlyRequested", (req) => {
req.reply((res) => {
const body = res.body;
const tv = body[0];
tv.tvPartiallyAvailable = true;
body[0] = tv;
res.send(body);
});
}).as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("158415");
card.verifyTitle("Pantanal");
card.status.should('contain.text', 'Partially Available');
});
});
it("Requested TV Is Pending", () => {
cy.requestAllTv(60574);
cy.intercept("GET", "**/v2/Requests/recentlyRequested", (req) => {
req.reply((res) => {
const body = res.body;
const tv = body[0];
tv.approved = false;
body[0] = tv;
res.send(body);
});
}).as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("60574");
card.verifyTitle("Peaky Blinders");
card.status.should('contain.text', 'Pending');
});
});
it("Requested TV Is Displayed", () => {
cy.requestAllTv(66732);
cy.intercept("GET", "**/v2/Requests/recentlyRequested").as("response");
Page.visit();
cy.wait("@response").then((_) => {
const card = Page.recentlyRequested.getRequest("66732");
card.verifyTitle("Stranger Things");
card.status.should('contain.text', 'Approved'); // Because admin auto request
});
});
});

View file

@ -12,8 +12,8 @@ const langs = [
];
langs.forEach((l) => {
it(`Change language to ${l.code}, UI should update`, () => {
cy.intercept('POST','/language').as('langSave');
it.only(`Change language to ${l.code}, UI should update`, () => {
cy.intercept('POST','**/language').as('langSave');
Page.visit();
Page.profile.languageSelectBox.click();

View file

@ -28,7 +28,7 @@ describe('User Management Page', () => {
// Setup the form
cy.get('#username').type(username);
cy.get('#alias').type("alias1");
cy.get('#emailAddress').type(username + "@emailaddress.com");
cy.get('#emailAddress').type(username + "@emailaddress.com", { force: true });
cy.get('#password').type("password");
cy.get('#confirmPass').type("password");
@ -54,7 +54,7 @@ describe('User Management Page', () => {
// Setup the form
cy.get('#username').type("user1");
cy.get('#alias').type("alias1");
cy.get('#emailAddress').type("user1@emailaddress.com");
cy.get('#emailAddress').type("user1@emailaddress.com", { force: true });
cy.get('#password').type("password");
cy.get('#confirmPass').type("password");
@ -72,7 +72,7 @@ describe('User Management Page', () => {
// Setup the form
cy.get('#username').type("user1");
cy.get('#alias').type("alias1");
cy.get('#emailAddress').type("user1@emailaddress.com");
cy.get('#emailAddress').type("user1@emailaddress.com", { force: true });
cy.get('#password').type("password");
cy.get('#confirmPass').type("pass22word");