Fixing composed slug handling & page field serialization

This commit is contained in:
Zoe Roux 2021-03-07 21:20:03 +01:00
parent fab9a3f6a1
commit b3fdee4bcd
11 changed files with 152 additions and 56 deletions

View File

@ -109,6 +109,8 @@ namespace Kyoo.Controllers
public interface IShowRepository : IRepository<Show> public interface IShowRepository : IRepository<Show>
{ {
Task AddShowLink(int showID, int? libraryID, int? collectionID); Task AddShowLink(int showID, int? libraryID, int? collectionID);
Task<string> GetSlug(int showID);
} }
public interface ISeasonRepository : IRepository<Season> public interface ISeasonRepository : IRepository<Season>

View File

@ -1,7 +0,0 @@
using System;
namespace Kyoo.Models.Attributes
{
[AttributeUsage(AttributeTargets.Class)]
public class ComposedSlugAttribute : Attribute { }
}

View File

@ -5,11 +5,11 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
[ComposedSlug]
public class Episode : IResource, IOnMerge public class Episode : IResource, IOnMerge
{ {
public int ID { get; set; } public int ID { get; set; }
public string Slug => Show != null ? GetSlug(Show.Slug, SeasonNumber, EpisodeNumber) : ID.ToString(); public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber);
[SerializeIgnore] public string ShowSlug { private get; set; }
[SerializeIgnore] public int ShowID { get; set; } [SerializeIgnore] public int ShowID { get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
[SerializeIgnore] public int? SeasonID { get; set; } [SerializeIgnore] public int? SeasonID { get; set; }
@ -30,9 +30,7 @@ namespace Kyoo.Models
[LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; } [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[LoadableRelation] public virtual ICollection<Track> Tracks { get; set; } [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; }
public string ShowTitle => Show?.Title;
public Episode() { } public Episode() { }
@ -78,6 +76,8 @@ namespace Kyoo.Models
public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber) public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber)
{ {
if (showSlug == null)
throw new ArgumentException("Show's slug is null. Can't find episode's slug.");
if (seasonNumber == -1) if (seasonNumber == -1)
return showSlug; return showSlug;
return $"{showSlug}-s{seasonNumber}e{episodeNumber}"; return $"{showSlug}-s{seasonNumber}e{episodeNumber}";

View File

@ -4,15 +4,16 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
[ComposedSlug]
public class Season : IResource public class Season : IResource
{ {
public int ID { get; set; } public int ID { get; set; }
public string Slug => $"{ShowSlug}-s{SeasonNumber}";
[SerializeIgnore] public int ShowID { get; set; } [SerializeIgnore] public int ShowID { get; set; }
[SerializeIgnore] public string ShowSlug { private get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
public int SeasonNumber { get; set; } = -1; public int SeasonNumber { get; set; } = -1;
public string Slug => Show != null ? $"{Show.Slug}-s{SeasonNumber}" : ID.ToString();
public string Title { get; set; } public string Title { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
@ -21,7 +22,6 @@ namespace Kyoo.Models
public string Thumb => $"/api/seasons/{Slug}/thumb"; public string Thumb => $"/api/seasons/{Slug}/thumb";
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; } [LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
public Season() { } public Season() { }

View File

@ -25,7 +25,7 @@ namespace Kyoo.Models
public class WatchItem public class WatchItem
{ {
public readonly int EpisodeID = -1; public readonly int EpisodeID;
public string ShowTitle; public string ShowTitle;
public string ShowSlug; public string ShowSlug;
@ -41,9 +41,9 @@ namespace Kyoo.Models
public string Container; public string Container;
public Track Video; public Track Video;
public IEnumerable<Track> Audios; public ICollection<Track> Audios;
public IEnumerable<Track> Subtitles; public ICollection<Track> Subtitles;
public IEnumerable<Chapter> Chapters; public ICollection<Chapter> Chapters;
public WatchItem() { } public WatchItem() { }
@ -78,8 +78,8 @@ namespace Kyoo.Models
DateTime? releaseDate, DateTime? releaseDate,
string path, string path,
Track video, Track video,
IEnumerable<Track> audios, ICollection<Track> audios,
IEnumerable<Track> subtitles) ICollection<Track> subtitles)
: this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path) : this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path)
{ {
Video = video; Video = video;
@ -120,8 +120,8 @@ namespace Kyoo.Models
ep.ReleaseDate, ep.ReleaseDate,
ep.Path, ep.Path,
ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
ep.Tracks.Where(x => x.Type == StreamType.Audio), ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
ep.Tracks.Where(x => x.Type == StreamType.Subtitle)) ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray())
{ {
IsMovie = show.IsMovie, IsMovie = show.IsMovie,
PreviousEpisode = previous, PreviousEpisode = previous,
@ -130,7 +130,7 @@ namespace Kyoo.Models
}; };
} }
private static async Task<IEnumerable<Chapter>> GetChapters(string episodePath) private static async Task<ICollection<Chapter>> GetChapters(string episodePath)
{ {
string path = PathIO.Combine( string path = PathIO.Combine(
PathIO.GetDirectoryName(episodePath)!, PathIO.GetDirectoryName(episodePath)!,

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections;
using System.Reflection; using System.Reflection;
using Kyoo.Models;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
@ -40,8 +42,13 @@ namespace Kyoo.Controllers
protected override JsonContract CreateContract(Type objectType) protected override JsonContract CreateContract(Type objectType)
{ {
JsonContract contract = base.CreateContract(objectType); JsonContract contract = base.CreateContract(objectType);
contract.OnSerializingCallbacks.Add((_, _) => _depth++); if (Utility.GetGenericDefinition(objectType, typeof(Page<>)) == null
contract.OnSerializedCallbacks.Add((_, _) => _depth--); && !objectType.IsAssignableTo(typeof(IEnumerable)))
{
contract.OnSerializingCallbacks.Add((_, _) => _depth++);
contract.OnSerializedCallbacks.Add((_, _) => _depth--);
}
return contract; return contract;
} }
} }

View File

@ -209,7 +209,7 @@ namespace Kyoo.Controllers
{ {
if (string.IsNullOrEmpty(resource.Slug)) if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("Resource can't have null as a slug."); throw new ArgumentException("Resource can't have null as a slug.");
if (int.TryParse(resource.Slug, out int _) && typeof(T).GetCustomAttribute<ComposedSlugAttribute>() == null) if (int.TryParse(resource.Slug, out int _))
{ {
try try
{ {

View File

@ -5,7 +5,6 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
@ -15,13 +14,16 @@ namespace Kyoo.Controllers
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
private readonly IShowRepository _shows;
protected override Expression<Func<Episode, object>> DefaultSort => x => x.EpisodeNumber; protected override Expression<Func<Episode, object>> DefaultSort => x => x.EpisodeNumber;
public EpisodeRepository(DatabaseContext database, IProviderRepository providers) : base(database) public EpisodeRepository(DatabaseContext database, IProviderRepository providers, IShowRepository shows)
: base(database)
{ {
_database = database; _database = database;
_providers = providers; _providers = providers;
_shows = shows;
} }
@ -32,6 +34,7 @@ namespace Kyoo.Controllers
_disposed = true; _disposed = true;
_database.Dispose(); _database.Dispose();
_providers.Dispose(); _providers.Dispose();
_shows.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
@ -42,6 +45,15 @@ namespace Kyoo.Controllers
_disposed = true; _disposed = true;
await _database.DisposeAsync(); await _database.DisposeAsync();
await _providers.DisposeAsync(); await _providers.DisposeAsync();
await _shows.DisposeAsync();
}
public override async Task<Episode> Get(int id)
{
Episode ret = await base.Get(id);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
} }
public override Task<Episode> Get(string slug) public override Task<Episode> Get(string slug)
@ -54,45 +66,81 @@ namespace Kyoo.Controllers
int.Parse(match.Groups["season"].Value), int.Parse(match.Groups["season"].Value),
int.Parse(match.Groups["episode"].Value)); int.Parse(match.Groups["episode"].Value));
} }
public Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber) public override async Task<Episode> Get(Expression<Func<Episode, bool>> predicate)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Episode ret = await base.Get(predicate);
&& x.SeasonNumber == seasonNumber if (ret != null)
&& x.EpisodeNumber == episodeNumber); ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
} }
public Task<Episode> Get(int showID, int seasonNumber, int episodeNumber) public async Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber && x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber); && x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
} }
public Task<Episode> Get(int seasonID, int episodeNumber) public async Task<Episode> Get(int showID, int seasonNumber, int episodeNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.EpisodeNumber == episodeNumber); && x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
} }
public Task<Episode> GetAbsolute(int showID, int absoluteNumber) public async Task<Episode> Get(int seasonID, int episodeNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID
&& x.AbsoluteNumber == absoluteNumber); && x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
} }
public Task<Episode> GetAbsolute(string showSlug, int absoluteNumber) public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.AbsoluteNumber == absoluteNumber); && x.AbsoluteNumber == absoluteNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
}
public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
} }
public override async Task<ICollection<Episode>> Search(string query) public override async Task<ICollection<Episode>> Search(string query)
{ {
return await _database.Episodes List<Episode> episodes = await _database.Episodes
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Title, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
foreach (Episode episode in episodes)
episode.ShowSlug = await _shows.GetSlug(episode.ShowID);
return episodes;
}
public override async Task<ICollection<Episode>> GetAll(Expression<Func<Episode, bool>> where = null,
Sort<Episode> sort = default,
Pagination limit = default)
{
ICollection<Episode> episodes = await base.GetAll(where, sort, limit);
foreach (Episode episode in episodes)
episode.ShowSlug = await _shows.GetSlug(episode.ShowID);
return episodes;
} }
public override async Task<Episode> Create(Episode obj) public override async Task<Episode> Create(Episode obj)

View File

@ -5,7 +5,6 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -16,17 +15,20 @@ namespace Kyoo.Controllers
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
private readonly IShowRepository _shows;
private readonly Lazy<IEpisodeRepository> _episodes; private readonly Lazy<IEpisodeRepository> _episodes;
protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber; protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber;
public SeasonRepository(DatabaseContext database, public SeasonRepository(DatabaseContext database,
IProviderRepository providers, IProviderRepository providers,
IShowRepository shows,
IServiceProvider services) IServiceProvider services)
: base(database) : base(database)
{ {
_database = database; _database = database;
_providers = providers; _providers = providers;
_shows = shows;
_episodes = new Lazy<IEpisodeRepository>(services.GetRequiredService<IEpisodeRepository>); _episodes = new Lazy<IEpisodeRepository>(services.GetRequiredService<IEpisodeRepository>);
} }
@ -38,6 +40,7 @@ namespace Kyoo.Controllers
_disposed = true; _disposed = true;
_database.Dispose(); _database.Dispose();
_providers.Dispose(); _providers.Dispose();
_shows.Dispose();
if (_episodes.IsValueCreated) if (_episodes.IsValueCreated)
_episodes.Value.Dispose(); _episodes.Value.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
@ -50,10 +53,27 @@ namespace Kyoo.Controllers
_disposed = true; _disposed = true;
await _database.DisposeAsync(); await _database.DisposeAsync();
await _providers.DisposeAsync(); await _providers.DisposeAsync();
await _shows.DisposeAsync();
if (_episodes.IsValueCreated) if (_episodes.IsValueCreated)
await _episodes.Value.DisposeAsync(); await _episodes.Value.DisposeAsync();
} }
public override async Task<Season> Get(int id)
{
Season ret = await base.Get(id);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
public override async Task<Season> Get(Expression<Func<Season, bool>> predicate)
{
Season ret = await base.Get(predicate);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
public override Task<Season> Get(string slug) public override Task<Season> Get(string slug)
{ {
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)"); Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)");
@ -63,24 +83,43 @@ namespace Kyoo.Controllers
return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value));
} }
public Task<Season> Get(int showID, int seasonNumber) public async Task<Season> Get(int showID, int seasonNumber)
{ {
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber); && x.SeasonNumber == seasonNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
} }
public Task<Season> Get(string showSlug, int seasonNumber) public async Task<Season> Get(string showSlug, int seasonNumber)
{ {
return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber); && x.SeasonNumber == seasonNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
} }
public override async Task<ICollection<Season>> Search(string query) public override async Task<ICollection<Season>> Search(string query)
{ {
return await _database.Seasons List<Season> seasons = await _database.Seasons
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Title, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
foreach (Season season in seasons)
season.ShowSlug = await _shows.GetSlug(season.ShowID);
return seasons;
}
public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null,
Sort<Season> sort = default,
Pagination limit = default)
{
ICollection<Season> seasons = await base.GetAll(where, sort, limit);
foreach (Season season in seasons)
season.ShowSlug = await _shows.GetSlug(season.ShowID);
return seasons;
} }
public override async Task<Season> Create(Season obj) public override async Task<Season> Create(Season obj)

View File

@ -151,6 +151,13 @@ namespace Kyoo.Controllers
} }
} }
public Task<string> GetSlug(int showID)
{
return _database.Shows.Where(x => x.ID == showID)
.Select(x => x.Slug)
.FirstOrDefaultAsync();
}
public override async Task Delete(Show obj) public override async Task Delete(Show obj)
{ {
if (obj == null) if (obj == null)

View File

@ -82,7 +82,7 @@ namespace Kyoo.Controllers
{ {
if (obj.EpisodeID <= 0) if (obj.EpisodeID <= 0)
{ {
obj.EpisodeID = obj.Episode?.ID ?? -1; obj.EpisodeID = obj.Episode?.ID ?? 0;
if (obj.EpisodeID <= 0) if (obj.EpisodeID <= 0)
throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID})."); throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID}).");
} }