mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-06 13:11:13 -07:00
feat: ✨ Recently Requested on Discover Page (#4387)
This commit is contained in:
parent
26ac75f0c2
commit
44d38fbaae
46 changed files with 3785 additions and 3567 deletions
205
src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs
Normal file
205
src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
21
src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs
Normal file
21
src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
12
src/Ombi.Core/Services/IRecentlyRequestedService.cs
Normal file
12
src/Ombi.Core/Services/IRecentlyRequestedService.cs
Normal 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);
|
||||
}
|
||||
}
|
146
src/Ombi.Core/Services/RecentlyRequestedService.cs
Normal file
146
src/Ombi.Core/Services/RecentlyRequestedService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
44
src/Ombi.TheMovieDbApi/Models/TvImages.cs
Normal file
44
src/Ombi.TheMovieDbApi/Models/TvImages.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
|
||||
body {
|
||||
background: #0f171f;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
|
@ -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 = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
};
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./image-background/image-background.component";
|
||||
export * from "./image/image.component";
|
||||
export * from "./detailed-card/detailed-card.component";
|
|
@ -1,3 +1,4 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { MomentModule } from "ngx-moment";
|
||||
|
||||
export const OmbiCommonModules = [ CommonModule ];
|
||||
export const OmbiCommonModules = [ CommonModule, MomentModule ];
|
|
@ -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
|
||||
}
|
||||
];
|
|
@ -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>
|
|
@ -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[] = [
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import { ImageComponent } from 'app/components';
|
|||
MatButtonToggleModule,
|
||||
InfiniteScrollModule,
|
||||
SkeletonModule,
|
||||
ImageComponent
|
||||
ImageComponent,
|
||||
],
|
||||
declarations: [
|
||||
...fromComponents.components
|
||||
|
|
18
src/Ombi/ClientApp/src/app/interfaces/IRecentlyRequested.ts
Normal file
18
src/Ombi/ClientApp/src/app/interfaces/IRecentlyRequested.ts
Normal 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;
|
||||
}
|
|
@ -21,3 +21,4 @@ export * from "./IVote";
|
|||
export * from "./IFailedRequests";
|
||||
export * from "./IHub";
|
||||
export * from "./ITester";
|
||||
export * from "./IRecentlyRequested";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
164
tests/cypress/tests/discover/discover-recently-requested.spec.ts
Normal file
164
tests/cypress/tests/discover/discover-recently-requested.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue