Merge pull request #20 from AnonymusRaccoon/dev

Dev
This commit is contained in:
Zoe Roux 2021-03-24 18:14:36 +01:00
commit adb6bdb3f2
101 changed files with 4245 additions and 3269 deletions

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Controllers
{
public interface IFileManager
{
public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false);
public StreamReader GetReader([NotNull] string path);
public Task<ICollection<string>> ListFiles([NotNull] string path);
public Task<bool> Exists([NotNull] string path);
// TODO find a way to handle Transmux/Transcode with this system.
public string GetExtraDirectory(Show show);
public string GetExtraDirectory(Season season);
public string GetExtraDirectory(Episode episode);
}
}

View File

@ -35,6 +35,7 @@ namespace Kyoo.Controllers
Task<Track> GetTrack(int id); Task<Track> GetTrack(int id);
Task<Studio> GetStudio(int id); Task<Studio> GetStudio(int id);
Task<People> GetPeople(int id); Task<People> GetPeople(int id);
Task<ProviderID> GetProvider(int id);
// Get by slug // Get by slug
Task<Library> GetLibrary(string slug); Task<Library> GetLibrary(string slug);
@ -49,6 +50,7 @@ namespace Kyoo.Controllers
Task<Genre> GetGenre(string slug); Task<Genre> GetGenre(string slug);
Task<Studio> GetStudio(string slug); Task<Studio> GetStudio(string slug);
Task<People> GetPeople(string slug); Task<People> GetPeople(string slug);
Task<ProviderID> GetProvider(string slug);
// Get by predicate // Get by predicate
Task<Library> GetLibrary(Expression<Func<Library, bool>> where); Task<Library> GetLibrary(Expression<Func<Library, bool>> where);
@ -61,6 +63,19 @@ namespace Kyoo.Controllers
Task<Studio> GetStudio(Expression<Func<Studio, bool>> where); Task<Studio> GetStudio(Expression<Func<Studio, bool>> where);
Task<People> GetPerson(Expression<Func<People, bool>> where); Task<People> GetPerson(Expression<Func<People, bool>> where);
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
where T : class, IResource
where T2 : class, IResource, new();
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource
where T2 : class, new();
Task<T> Load<T>([NotNull] T obj, string memberName)
where T : class, IResource;
Task Load([NotNull] IResource obj, string memberName);
// Library Items relations // Library Items relations
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id, Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
@ -104,25 +119,25 @@ namespace Kyoo.Controllers
) => GetPeopleFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit); ) => GetPeopleFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit);
// Show Role relations // Show Role relations
Task<ICollection<ShowRole>> GetRolesFromPeople(int showID, Task<ICollection<PeopleRole>> GetRolesFromPeople(int showID,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<ShowRole>> GetRolesFromPeople(int showID, Task<ICollection<PeopleRole>> GetRolesFromPeople(int showID,
[Optional] Expression<Func<ShowRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<ShowRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetRolesFromPeople(showID, where, new Sort<ShowRole>(sort), limit); ) => GetRolesFromPeople(showID, where, new Sort<PeopleRole>(sort), limit);
Task<ICollection<ShowRole>> GetRolesFromPeople(string showSlug, Task<ICollection<PeopleRole>> GetRolesFromPeople(string showSlug,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<ShowRole>> GetRolesFromPeople(string showSlug, Task<ICollection<PeopleRole>> GetRolesFromPeople(string showSlug,
[Optional] Expression<Func<ShowRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<ShowRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetRolesFromPeople(showSlug, where, new Sort<ShowRole>(sort), limit); ) => GetRolesFromPeople(showSlug, where, new Sort<PeopleRole>(sort), limit);
// Helpers // Helpers
Task AddShowLink(int showID, int? libraryID, int? collectionID); Task AddShowLink(int showID, int? libraryID, int? collectionID);
@ -237,9 +252,8 @@ namespace Kyoo.Controllers
Task<Studio> EditStudio(Studio studio, bool resetOld); Task<Studio> EditStudio(Studio studio, bool resetOld);
Task<People> EditPeople(People people, bool resetOld); Task<People> EditPeople(People people, bool resetOld);
// Delete values // Delete values
Task DelteLibrary(Library library); Task DeleteLibrary(Library library);
Task DeleteCollection(Collection collection); Task DeleteCollection(Collection collection);
Task DeleteShow(Show show); Task DeleteShow(Show show);
Task DeleteSeason(Season season); Task DeleteSeason(Season season);
@ -250,7 +264,7 @@ namespace Kyoo.Controllers
Task DeletePeople(People people); Task DeletePeople(People people);
//Delete by slug //Delete by slug
Task DelteLibrary(string slug); Task DeleteLibrary(string slug);
Task DeleteCollection(string slug); Task DeleteCollection(string slug);
Task DeleteShow(string slug); Task DeleteShow(string slug);
Task DeleteSeason(string slug); Task DeleteSeason(string slug);
@ -261,7 +275,7 @@ namespace Kyoo.Controllers
Task DeletePeople(string slug); Task DeletePeople(string slug);
//Delete by id //Delete by id
Task DelteLibrary(int id); Task DeleteLibrary(int id);
Task DeleteCollection(int id); Task DeleteCollection(int id);
Task DeleteShow(int id); Task DeleteShow(int id);
Task DeleteSeason(int id); Task DeleteSeason(int id);

View File

@ -11,8 +11,8 @@ namespace Kyoo.Controllers
Task<Collection> GetCollectionFromName(string name); Task<Collection> GetCollectionFromName(string name);
Task<Show> GetShowByID(Show show); Task<Show> GetShowByID(Show show);
Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie); Task<ICollection<Show>> SearchShows(string showName, bool isMovie);
Task<IEnumerable<PeopleRole>> GetPeople(Show show); Task<ICollection<PeopleRole>> GetPeople(Show show);
Task<Season> GetSeason(Show show, int seasonNumber); Task<Season> GetSeason(Show show, int seasonNumber);

View File

@ -12,6 +12,6 @@ namespace Kyoo.Controllers
Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library); Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library);
Task<Season> GetSeason(Show show, int seasonNumber, Library library); Task<Season> GetSeason(Show show, int seasonNumber, Library library);
Task<Episode> GetEpisode(Show show, string episodePath, int seasonNumber, int episodeNumber, int absoluteNumber, Library library); Task<Episode> GetEpisode(Show show, string episodePath, int seasonNumber, int episodeNumber, int absoluteNumber, Library library);
Task<IEnumerable<PeopleRole>> GetPeople(Show show, Library library); Task<ICollection<PeopleRole>> GetPeople(Show show, Library library);
} }
} }

View File

@ -20,7 +20,7 @@ namespace Kyoo.Controllers
AfterID = afterID; AfterID = afterID;
} }
public static implicit operator Pagination(int limit) => new Pagination(limit); public static implicit operator Pagination(int limit) => new(limit);
} }
public struct Sort<T> public struct Sort<T>
@ -30,14 +30,10 @@ namespace Kyoo.Controllers
public Sort(Expression<Func<T, object>> key, bool descendant = false) public Sort(Expression<Func<T, object>> key, bool descendant = false)
{ {
Key = ExpressionRewrite.Rewrite<Func<T, object>>(key); Key = key;
Descendant = descendant; Descendant = descendant;
if (Key == null || if (!Utility.IsPropertyExpression(Key))
Key.Body is MemberExpression ||
Key.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)Key.Body).Operand is MemberExpression)
return;
throw new ArgumentException("The given sort key is not valid."); throw new ArgumentException("The given sort key is not valid.");
} }
@ -58,7 +54,6 @@ namespace Kyoo.Controllers
Key = property.Type.IsValueType Key = property.Type.IsValueType
? Expression.Lambda<Func<T, object>>(Expression.Convert(property, typeof(object)), param) ? Expression.Lambda<Func<T, object>>(Expression.Convert(property, typeof(object)), param)
: Expression.Lambda<Func<T, object>>(property, param); : Expression.Lambda<Func<T, object>>(property, param);
Key = ExpressionRewrite.Rewrite<Func<T, object>>(Key);
Descendant = order switch Descendant = order switch
{ {
@ -68,11 +63,6 @@ namespace Kyoo.Controllers
_ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.")
}; };
} }
public Sort<TValue> To<TValue>()
{
return new Sort<TValue>(Key.Convert<Func<TValue, object>>(), Descendant);
}
} }
public interface IRepository<T> : IDisposable, IAsyncDisposable where T : class, IResource public interface IRepository<T> : IDisposable, IAsyncDisposable where T : class, IResource
@ -108,11 +98,14 @@ namespace Kyoo.Controllers
Task DeleteRange(IEnumerable<int> ids); Task DeleteRange(IEnumerable<int> ids);
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
Task DeleteRange(IEnumerable<string> slugs); Task DeleteRange(IEnumerable<string> slugs);
Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
} }
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>
@ -190,26 +183,36 @@ namespace Kyoo.Controllers
Pagination limit = default Pagination limit = default
) => GetFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit); ) => GetFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit);
Task<ICollection<ShowRole>> GetFromPeople(int showID, Task<ICollection<PeopleRole>> GetFromPeople(int showID,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<ShowRole>> GetFromPeople(int showID, Task<ICollection<PeopleRole>> GetFromPeople(int showID,
[Optional] Expression<Func<ShowRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<ShowRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromPeople(showID, where, new Sort<ShowRole>(sort), limit); ) => GetFromPeople(showID, where, new Sort<PeopleRole>(sort), limit);
Task<ICollection<ShowRole>> GetFromPeople(string showSlug, Task<ICollection<PeopleRole>> GetFromPeople(string showSlug,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<ShowRole>> GetFromPeople(string showSlug, Task<ICollection<PeopleRole>> GetFromPeople(string showSlug,
[Optional] Expression<Func<ShowRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<ShowRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromPeople(showSlug, where, new Sort<ShowRole>(sort), limit); ) => GetFromPeople(showSlug, where, new Sort<PeopleRole>(sort), limit);
} }
public interface IProviderRepository : IRepository<ProviderID> {} public interface IProviderRepository : IRepository<ProviderID>
{
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default);
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
Expression<Func<MetadataID, object>> sort,
Pagination limit = default
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit);
}
} }

View File

@ -1,14 +1,24 @@
using Kyoo.Models; using Kyoo.Models;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public interface IThumbnailsManager public interface IThumbnailsManager
{ {
Task<Show> Validate(Show show, bool alwaysDownload = false); Task Validate(Show show, bool alwaysDownload = false);
Task<Season> Validate(Season season, bool alwaysDownload = false); Task Validate(Season season, bool alwaysDownload = false);
Task<Episode> Validate(Episode episode, bool alwaysDownload = false); Task Validate(Episode episode, bool alwaysDownload = false);
Task<IEnumerable<PeopleRole>> Validate(IEnumerable<PeopleRole> actors, bool alwaysDownload = false); Task Validate(People actors, bool alwaysDownload = false);
Task Validate(ProviderID actors, bool alwaysDownload = false);
Task<string> GetShowPoster([NotNull] Show show);
Task<string> GetShowLogo([NotNull] Show show);
Task<string> GetShowBackdrop([NotNull] Show show);
Task<string> GetSeasonPoster([NotNull] Season season);
Task<string> GetEpisodeThumb([NotNull] Episode episode);
Task<string> GetPeoplePoster([NotNull] People people);
Task<string> GetProviderLogo([NotNull] ProviderID provider);
} }
} }

View File

@ -1,12 +1,11 @@
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Watch;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public interface ITranscoder public interface ITranscoder
{ {
Task<Track[]> ExtractInfos(string path); Task<Track[]> ExtractInfos(Episode episode, bool reextract);
Task<string> Transmux(Episode episode); Task<string> Transmux(Episode episode);
Task<string> Transcode(Episode episode); Task<string> Transcode(Episode episode);
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
@ -130,6 +131,11 @@ namespace Kyoo.Controllers
return PeopleRepository.Get(id); return PeopleRepository.Get(id);
} }
public Task<ProviderID> GetProvider(int id)
{
return ProviderRepository.Get(id);
}
public Task<Library> GetLibrary(string slug) public Task<Library> GetLibrary(string slug)
{ {
return LibraryRepository.Get(slug); return LibraryRepository.Get(slug);
@ -190,6 +196,11 @@ namespace Kyoo.Controllers
return PeopleRepository.Get(slug); return PeopleRepository.Get(slug);
} }
public Task<ProviderID> GetProvider(string slug)
{
return ProviderRepository.Get(slug);
}
public Task<Library> GetLibrary(Expression<Func<Library, bool>> where) public Task<Library> GetLibrary(Expression<Func<Library, bool>> where)
{ {
return LibraryRepository.Get(where); return LibraryRepository.Get(where);
@ -235,6 +246,195 @@ namespace Kyoo.Controllers
return PeopleRepository.Get(where); return PeopleRepository.Get(where);
} }
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
where T : class, IResource
where T2 : class, IResource, new()
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member));
}
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource
where T2 : class, new()
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member));
}
public async Task<T> Load<T>(T obj, string member)
where T : class, IResource
{
await Load(obj as IResource, member);
return obj;
}
private async Task SetRelation<T1, T2>(T1 obj,
Task<ICollection<T2>> loader,
Action<T1, ICollection<T2>> setter,
Action<T2, T1> inverse)
{
ICollection<T2> loaded = await loader;
setter(obj, loaded);
foreach (T2 item in loaded)
inverse(item, obj);
}
public Task Load(IResource obj, string member)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return (obj, member) switch
{
(Library l, nameof(Library.Providers)) => ProviderRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
.Then(x => l.Providers = x),
(Library l, nameof(Library.Shows)) => ShowRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
.Then(x => l.Shows = x),
(Library l, nameof(Library.Collections)) => CollectionRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
.Then(x => l.Collections = x),
(Collection c, nameof(Library.Shows)) => ShowRepository
.GetAll(x => x.Collections.Any(y => y.ID == obj.ID))
.Then(x => c.Shows = x),
(Collection c, nameof(Collection.Libraries)) => LibraryRepository
.GetAll(x => x.Collections.Any(y => y.ID == obj.ID))
.Then(x => c.Libraries = x),
(Show s, nameof(Show.ExternalIDs)) => SetRelation(s,
ProviderRepository.GetMetadataID(x => x.ShowID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.Show = y; x.ShowID = y.ID; }),
(Show s, nameof(Show.Genres)) => GenreRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => s.Genres = x),
(Show s, nameof(Show.People)) => PeopleRepository
.GetFromShow(obj.ID)
.Then(x => s.People = x),
(Show s, nameof(Show.Seasons)) => SetRelation(s,
SeasonRepository.GetAll(x => x.Show.ID == obj.ID),
(x, y) => x.Seasons = y,
(x, y) => { x.Show = y; x.ShowID = y.ID; }),
(Show s, nameof(Show.Episodes)) => SetRelation(s,
EpisodeRepository.GetAll(x => x.Show.ID == obj.ID),
(x, y) => x.Episodes = y,
(x, y) => { x.Show = y; x.ShowID = y.ID; }),
(Show s, nameof(Show.Libraries)) => LibraryRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => s.Libraries = x),
(Show s, nameof(Show.Collections)) => CollectionRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => s.Collections = x),
(Show s, nameof(Show.Studio)) => StudioRepository
.Get(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x =>
{
s.Studio = x;
s.StudioID = x?.ID ?? 0;
}),
(Season s, nameof(Season.ExternalIDs)) => SetRelation(s,
ProviderRepository.GetMetadataID(x => x.SeasonID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.Season = y; x.SeasonID = y.ID; }),
(Season s, nameof(Season.Episodes)) => SetRelation(s,
EpisodeRepository.GetAll(x => x.Season.ID == obj.ID),
(x, y) => x.Episodes = y,
(x, y) => { x.Season = y; x.SeasonID = y.ID; }),
(Season s, nameof(Season.Show)) => ShowRepository
.Get(x => x.Seasons.Any(y => y.ID == obj.ID))
.Then(x =>
{
s.Show = x;
s.ShowID = x?.ID ?? 0;
}),
(Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e,
ProviderRepository.GetMetadataID(x => x.EpisodeID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.Episode = y; x.EpisodeID = y.ID; }),
(Episode e, nameof(Episode.Tracks)) => SetRelation(e,
TrackRepository.GetAll(x => x.Episode.ID == obj.ID),
(x, y) => x.Tracks = y,
(x, y) => { x.Episode = y; x.EpisodeID = y.ID; }),
(Episode e, nameof(Episode.Show)) => ShowRepository
.Get(x => x.Episodes.Any(y => y.ID == obj.ID))
.Then(x =>
{
e.Show = x;
e.ShowID = x?.ID ?? 0;
}),
(Episode e, nameof(Episode.Season)) => SeasonRepository
.Get(x => x.Episodes.Any(y => y.ID == e.ID))
.Then(x =>
{
e.Season = x;
e.SeasonID = x?.ID ?? 0;
}),
(Track t, nameof(Track.Episode)) => EpisodeRepository
.Get(x => x.Tracks.Any(y => y.ID == obj.ID))
.Then(x =>
{
t.Episode = x;
t.EpisodeID = x?.ID ?? 0;
}),
(Genre g, nameof(Genre.Shows)) => ShowRepository
.GetAll(x => x.Genres.Any(y => y.ID == obj.ID))
.Then(x => g.Shows = x),
(Studio s, nameof(Studio.Shows)) => ShowRepository
.GetAll(x => x.Studio.ID == obj.ID)
.Then(x => s.Shows = x),
(People p, nameof(People.ExternalIDs)) => SetRelation(p,
ProviderRepository.GetMetadataID(x => x.PeopleID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.People = y; x.PeopleID = y.ID; }),
(People p, nameof(People.Roles)) => PeopleRepository
.GetFromPeople(obj.ID)
.Then(x => p.Roles = x),
(ProviderID p, nameof(ProviderID.Libraries)) => LibraryRepository
.GetAll(x => x.Providers.Any(y => y.ID == obj.ID))
.Then(x => p.Libraries = x),
_ => throw new ArgumentException($"Couldn't find a way to load {member} of {obj.Slug}.")
};
}
public Task<ICollection<Library>> GetLibraries(Expression<Func<Library, bool>> where = null, public Task<ICollection<Library>> GetLibraries(Expression<Func<Library, bool>> where = null,
Sort<Library> sort = default, Sort<Library> sort = default,
Pagination page = default) Pagination page = default)
@ -337,17 +537,17 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromShow(showSlug, where, sort, limit); return PeopleRepository.GetFromShow(showSlug, where, sort, limit);
} }
public Task<ICollection<ShowRole>> GetRolesFromPeople(int id, public Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
return PeopleRepository.GetFromPeople(id, where, sort, limit); return PeopleRepository.GetFromPeople(id, where, sort, limit);
} }
public Task<ICollection<ShowRole>> GetRolesFromPeople(string slug, public Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
return PeopleRepository.GetFromPeople(slug, where, sort, limit); return PeopleRepository.GetFromPeople(slug, where, sort, limit);
@ -540,7 +740,7 @@ namespace Kyoo.Controllers
return PeopleRepository.Edit(people, resetOld); return PeopleRepository.Edit(people, resetOld);
} }
public Task DelteLibrary(Library library) public Task DeleteLibrary(Library library)
{ {
return LibraryRepository.Delete(library); return LibraryRepository.Delete(library);
} }
@ -585,7 +785,7 @@ namespace Kyoo.Controllers
return PeopleRepository.Delete(people); return PeopleRepository.Delete(people);
} }
public Task DelteLibrary(string library) public Task DeleteLibrary(string library)
{ {
return LibraryRepository.Delete(library); return LibraryRepository.Delete(library);
} }
@ -630,7 +830,7 @@ namespace Kyoo.Controllers
return PeopleRepository.Delete(people); return PeopleRepository.Delete(people);
} }
public Task DelteLibrary(int library) public Task DeleteLibrary(int library)
{ {
return LibraryRepository.Delete(library); return LibraryRepository.Delete(library);
} }

View File

@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Kyoo
{
public class ExpressionRewriteAttribute : Attribute
{
public string Link { get; }
public string Inner { get; }
public ExpressionRewriteAttribute(string link, string inner = null)
{
Link = link;
Inner = inner;
}
}
public class ExpressionRewrite : ExpressionVisitor
{
private string _inner;
private readonly List<(string inner, ParameterExpression param, ParameterExpression newParam)> _innerRewrites;
private ExpressionRewrite()
{
_innerRewrites = new List<(string, ParameterExpression, ParameterExpression)>();
}
public static Expression Rewrite(Expression expression)
{
return new ExpressionRewrite().Visit(expression);
}
public static Expression<T> Rewrite<T>(Expression expression) where T : Delegate
{
return (Expression<T>)new ExpressionRewrite().Visit(expression);
}
protected override Expression VisitMember(MemberExpression node)
{
(string inner, _, ParameterExpression p) = _innerRewrites.FirstOrDefault(x => x.param == node.Expression);
if (inner != null)
{
Expression param = p;
foreach (string accessor in inner.Split('.'))
param = Expression.Property(param, accessor);
node = Expression.Property(param, node.Member.Name);
}
// Can't use node.Member directly because we want to support attribute override
MemberInfo member = node.Expression.Type.GetProperty(node.Member.Name) ?? node.Member;
ExpressionRewriteAttribute attr = member!.GetCustomAttribute<ExpressionRewriteAttribute>();
if (attr == null)
return base.VisitMember(node);
Expression property = node.Expression;
foreach (string child in attr.Link.Split('.'))
property = Expression.Property(property, child);
if (property is MemberExpression expr)
Visit(expr.Expression);
_inner = attr.Inner;
return property;
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
(_, ParameterExpression oldParam, ParameterExpression param) = _innerRewrites
.FirstOrDefault(x => node.Parameters.Any(y => y == x.param));
if (param == null)
return base.VisitLambda(node);
ParameterExpression[] newParams = node.Parameters.Where(x => x != oldParam).Append(param).ToArray();
return Expression.Lambda(Visit(node.Body)!, newParams);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
int count = node.Arguments.Count;
if (node.Object != null)
count++;
if (count != 2)
return base.VisitMethodCall(node);
Expression instance = node.Object ?? node.Arguments.First();
Expression argument = node.Object != null
? node.Arguments.First()
: node.Arguments[1];
Type oldType = instance.Type;
instance = Visit(instance);
if (instance!.Type == oldType)
return base.VisitMethodCall(node);
if (_inner != null && argument is LambdaExpression lambda)
{
// TODO this type handler will usually work with IEnumerable & others but won't work with everything.
Type type = oldType.GetGenericArguments().First();
ParameterExpression oldParam = lambda.Parameters.FirstOrDefault(x => x.Type == type);
if (oldParam != null)
{
Type newType = instance.Type.GetGenericArguments().First();
ParameterExpression newParam = Expression.Parameter(newType, oldParam.Name);
_innerRewrites.Add((_inner, oldParam, newParam));
}
}
argument = Visit(argument);
// TODO this method handler may not work for some methods (ex: method taking a Fun<> method won't have good generic arguments)
MethodInfo method = node.Method.IsGenericMethod
? node.Method.GetGenericMethodDefinition().MakeGenericMethod(instance.Type.GetGenericArguments())
: node.Method;
return node.Object != null
? Expression.Call(instance, method!, argument)
: Expression.Call(null, method!, instance, argument!);
}
}
}

View File

@ -12,14 +12,17 @@
<Company>SDG</Company> <Company>SDG</Company>
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageVersion>1.0.22</PackageVersion> <PackageVersion>1.0.23</PackageVersion>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<DefineConstants>ENABLE_INTERNAL_LINKS</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
</ItemGroup> </ItemGroup>

View File

@ -1,7 +0,0 @@
using System;
namespace Kyoo.Models.Attributes
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class EditableRelation : Attribute { }
}

View File

@ -8,7 +8,4 @@ namespace Kyoo.Models.Attributes
{ {
void OnMerge(object merged); void OnMerge(object merged);
} }
public class JsonReadOnly : Attribute { }
public class JsonIgnore : JsonReadOnly { }
} }

View File

@ -0,0 +1,20 @@
using System;
namespace Kyoo.Models.Attributes
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class EditableRelationAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Property)]
public class LoadableRelationAttribute : Attribute
{
public string RelationID { get; }
public LoadableRelationAttribute() {}
public LoadableRelationAttribute(string relationID)
{
RelationID = relationID;
}
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace Kyoo.Models.Attributes
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeIgnoreAttribute : Attribute {}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DeserializeIgnoreAttribute : Attribute {}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeAsAttribute : Attribute
{
public string Format { get; }
public SerializeAsAttribute(string format)
{
Format = format;
}
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
@ -20,7 +21,8 @@ namespace Kyoo.Models
public string TrailerUrl { get; set; } public string TrailerUrl { get; set; }
public int? StartYear { get; set; } public int? StartYear { get; set; }
public int? EndYear { get; set; } public int? EndYear { get; set; }
public string Poster { get; set; } [SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; }
private string _type => Type == ItemType.Collection ? "collection" : "show";
public ItemType Type { get; set; } public ItemType Type { get; set; }
public LibraryItem() {} public LibraryItem() {}

View File

@ -0,0 +1,84 @@
using System;
using System.Linq.Expressions;
namespace Kyoo.Models
{
public class Link
{
public int FirstID { get; set; }
public int SecondID { get; set; }
public Link() {}
public Link(int firstID, int secondID)
{
FirstID = firstID;
SecondID = secondID;
}
public Link(IResource first, IResource second)
{
FirstID = first.ID;
SecondID = second.ID;
}
public static Link Create(IResource first, IResource second)
{
return new(first, second);
}
public static Link<T, T2> Create<T, T2>(T first, T2 second)
where T : class, IResource
where T2 : class, IResource
{
return new(first, second);
}
public static Link<T, T2> UCreate<T, T2>(T first, T2 second)
where T : class, IResource
where T2 : class, IResource
{
return new(first, second, true);
}
public static Expression<Func<Link, object>> PrimaryKey
{
get
{
return x => new {First = x.FirstID, Second = x.SecondID};
}
}
}
public class Link<T1, T2> : Link
where T1 : class, IResource
where T2 : class, IResource
{
public virtual T1 First { get; set; }
public virtual T2 Second { get; set; }
public Link() {}
public Link(T1 first, T2 second, bool privateItems = false)
: base(first, second)
{
if (privateItems)
return;
First = first;
Second = second;
}
public Link(int firstID, int secondID)
: base(firstID, secondID)
{ }
public new static Expression<Func<Link<T1, T2>, object>> PrimaryKey
{
get
{
return x => new {First = x.FirstID, Second = x.SecondID};
}
}
}
}

View File

@ -1,25 +1,24 @@
using System;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class MetadataID public class MetadataID
{ {
[JsonIgnore] public int ID { get; set; } [SerializeIgnore] public int ID { get; set; }
[JsonIgnore] public int ProviderID { get; set; } [SerializeIgnore] public int ProviderID { get; set; }
public virtual ProviderID Provider {get; set; } public virtual ProviderID Provider {get; set; }
[JsonIgnore] public int? ShowID { get; set; } [SerializeIgnore] public int? ShowID { get; set; }
[JsonIgnore] public virtual Show Show { get; set; } [SerializeIgnore] public virtual Show Show { get; set; }
[JsonIgnore] public int? EpisodeID { get; set; } [SerializeIgnore] public int? EpisodeID { get; set; }
[JsonIgnore] public virtual Episode Episode { get; set; } [SerializeIgnore] public virtual Episode Episode { get; set; }
[JsonIgnore] public int? SeasonID { get; set; } [SerializeIgnore] public int? SeasonID { get; set; }
[JsonIgnore] public virtual Season Season { get; set; } [SerializeIgnore] public virtual Season Season { get; set; }
[JsonIgnore] public int? PeopleID { get; set; } [SerializeIgnore] public int? PeopleID { get; set; }
[JsonIgnore] public virtual People People { get; set; } [SerializeIgnore] public virtual People People { get; set; }
public string DataID { get; set; } public string DataID { get; set; }
public string Link { get; set; } public string Link { get; set; }

View File

@ -1,46 +1,17 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class PeopleRole : IResource public class PeopleRole : IResource
{ {
[JsonIgnore] public int ID { get; set; } [SerializeIgnore] public int ID { get; set; }
[JsonIgnore] public int PeopleID { get; set; } [SerializeIgnore] public string Slug => ForPeople ? Show.Slug : People.Slug;
[JsonIgnore] public virtual People People { get; set; } [SerializeIgnore] public bool ForPeople;
[SerializeIgnore] public int PeopleID { get; set; }
[ExpressionRewrite(nameof(People) + "." + nameof(Models.People.Slug))] [SerializeIgnore] public virtual People People { get; set; }
public string Slug [SerializeIgnore] public int ShowID { get; set; }
{ [SerializeIgnore] public virtual Show Show { get; set; }
get => People.Slug;
set => People.Slug = value;
}
[ExpressionRewrite(nameof(People) + "."+ nameof(Models.People.Name))]
public string Name
{
get => People.Name;
set => People.Name = value;
}
[ExpressionRewrite(nameof(People) + "."+ nameof(Models.People.Poster))]
public string Poster
{
get => People.Poster;
set => People.Poster = value;
}
[ExpressionRewrite(nameof(People) + "."+ nameof(Models.People.ExternalIDs))]
public IEnumerable<MetadataID> ExternalIDs
{
get => People.ExternalIDs;
set => People.ExternalIDs = value;
}
[JsonIgnore] public int ShowID { get; set; }
[JsonIgnore] public virtual Show Show { get; set; }
public string Role { get; set; } public string Role { get; set; }
public string Type { get; set; } public string Type { get; set; }
@ -66,67 +37,4 @@ namespace Kyoo.Models
Type = type; Type = type;
} }
} }
public class ShowRole : IResource
{
public int ID { get; set; }
public string Role { get; set; }
public string Type { get; set; }
public string Slug { get; set; }
public string Title { get; set; }
public IEnumerable<string> Aliases { get; set; }
[JsonIgnore] public string Path { get; set; }
public string Overview { get; set; }
public Status? Status { get; set; }
public string TrailerUrl { get; set; }
public int? StartYear { get; set; }
public int? EndYear { get; set; }
public string Poster { get; set; }
public string Logo { get; set; }
public string Backdrop { get; set; }
public bool IsMovie { get; set; }
public ShowRole() {}
public ShowRole(PeopleRole x)
{
ID = x.ID;
Role = x.Role;
Type = x.Type;
Slug = x.Show.Slug;
Title = x.Show.Title;
Aliases = x.Show.Aliases;
Path = x.Show.Path;
Overview = x.Show.Overview;
Status = x.Show.Status;
TrailerUrl = x.Show.TrailerUrl;
StartYear = x.Show.StartYear;
EndYear = x.Show.EndYear;
Poster = x.Show.Poster;
Logo = x.Show.Logo;
Backdrop = x.Show.Backdrop;
IsMovie = x.Show.IsMovie;
}
public static Expression<Func<PeopleRole, ShowRole>> FromPeopleRole => x => new ShowRole
{
ID = x.ID,
Role = x.Role,
Type = x.Type,
Slug = x.Show.Slug,
Title = x.Show.Title,
Aliases = x.Show.Aliases,
Path = x.Show.Path,
Overview = x.Show.Overview,
Status = x.Show.Status,
TrailerUrl = x.Show.TrailerUrl,
StartYear = x.Show.StartYear,
EndYear = x.Show.EndYear,
Poster = x.Show.Poster,
Logo = x.Show.Logo,
Backdrop = x.Show.Backdrop,
IsMovie = x.Show.IsMovie
};
}
} }

View File

@ -5,6 +5,6 @@ namespace Kyoo.Models
public interface IPlugin public interface IPlugin
{ {
public string Name { get; } public string Name { get; }
public IEnumerable<ITask> Tasks { get; } public ICollection<ITask> Tasks { get; }
} }
} }

View File

@ -8,10 +8,15 @@ namespace Kyoo.Models
public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Poster { get; set; } [SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
[JsonIgnore] public virtual IEnumerable<Show> Shows { get; set; } [LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[JsonIgnore] public virtual IEnumerable<Library> Libraries { get; set; } [LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
#endif
public Collection() { } public Collection() { }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
@ -7,37 +8,28 @@ namespace Kyoo.Models
public class Episode : IResource, IOnMerge public class Episode : IResource, IOnMerge
{ {
public int ID { get; set; } public int ID { get; set; }
public int ShowID { get; set; } public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber);
[JsonIgnore] public virtual Show Show { get; set; } [SerializeIgnore] public string ShowSlug { private get; set; }
public int? SeasonID { get; set; } [SerializeIgnore] public int ShowID { get; set; }
[JsonIgnore] public virtual Season Season { get; set; } [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
[SerializeIgnore] public int? SeasonID { get; set; }
[LoadableRelation(nameof(SeasonID))] public virtual Season Season { get; set; }
public int SeasonNumber { get; set; } = -1; public int SeasonNumber { get; set; } = -1;
public int EpisodeNumber { get; set; } = -1; public int EpisodeNumber { get; set; } = -1;
public int AbsoluteNumber { get; set; } = -1; public int AbsoluteNumber { get; set; } = -1;
[JsonIgnore] public string Path { get; set; } [SerializeIgnore] public string Path { get; set; }
[SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public int Runtime { get; set; } //This runtime variable should be in minutes public int Runtime { get; set; } //This runtime variable should be in minutes
[JsonIgnore] public string Poster { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] public virtual IEnumerable<MetadataID> ExternalIDs { get; set; }
[JsonIgnore] public virtual IEnumerable<Track> Tracks { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; }
public string ShowTitle => Show.Title;
public string Slug => GetSlug(Show.Slug, SeasonNumber, EpisodeNumber);
public string Thumb
{
get
{
if (Show != null)
return "thumb/" + Slug;
return Poster;
}
}
public Episode() { } public Episode() { }
@ -49,7 +41,7 @@ namespace Kyoo.Models
string overview, string overview,
DateTime? releaseDate, DateTime? releaseDate,
int runtime, int runtime,
string poster, string thumb,
IEnumerable<MetadataID> externalIDs) IEnumerable<MetadataID> externalIDs)
{ {
SeasonNumber = seasonNumber; SeasonNumber = seasonNumber;
@ -59,8 +51,8 @@ namespace Kyoo.Models
Overview = overview; Overview = overview;
ReleaseDate = releaseDate; ReleaseDate = releaseDate;
Runtime = runtime; Runtime = runtime;
Poster = poster; Thumb = thumb;
ExternalIDs = externalIDs; ExternalIDs = externalIDs?.ToArray();
} }
public Episode(int showID, public Episode(int showID,
@ -75,23 +67,17 @@ namespace Kyoo.Models
int runtime, int runtime,
string poster, string poster,
IEnumerable<MetadataID> externalIDs) IEnumerable<MetadataID> externalIDs)
: this(seasonNumber, episodeNumber, absoluteNumber, title, overview, releaseDate, runtime, poster, externalIDs)
{ {
ShowID = showID; ShowID = showID;
SeasonID = seasonID; SeasonID = seasonID;
SeasonNumber = seasonNumber;
EpisodeNumber = episodeNumber;
AbsoluteNumber = absoluteNumber;
Path = path; Path = path;
Title = title;
Overview = overview;
ReleaseDate = releaseDate;
Runtime = runtime;
Poster = poster;
ExternalIDs = externalIDs;
} }
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

@ -5,11 +5,16 @@ namespace Kyoo.Models
{ {
public class Genre : IResource public class Genre : IResource
{ {
[JsonIgnore] public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
[JsonIgnore] public virtual IEnumerable<Show> Shows { get; set; } [LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> ShowLinks { get; set; }
#endif
public Genre() {} public Genre() {}

View File

@ -9,16 +9,6 @@ namespace Kyoo.Models
public string Slug { get; } public string Slug { get; }
} }
public interface IResourceLink<out T, out T2>
where T : IResource
where T2 : IResource
{
public T Parent { get; }
public int ParentID { get; }
public T2 Child { get; }
public int ChildID { get; }
}
public class ResourceComparer<T> : IEqualityComparer<T> where T : IResource public class ResourceComparer<T> : IEqualityComparer<T> where T : IResource
{ {
public bool Equals(T x, T y) public bool Equals(T x, T y)
@ -37,27 +27,4 @@ namespace Kyoo.Models
return HashCode.Combine(obj.ID, obj.Slug); return HashCode.Combine(obj.ID, obj.Slug);
} }
} }
public class LinkComparer<T, T1, T2> : IEqualityComparer<T>
where T : IResourceLink<T1, T2>
where T1 : IResource
where T2 : IResource
{
public bool Equals(T x, T y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null))
return false;
if (ReferenceEquals(y, null))
return false;
return Utility.LinkEquals(x.Parent, x.ParentID, y.Parent, y.ParentID)
&& Utility.LinkEquals(x.Child, x.ChildID, y.Child, y.ChildID);
}
public int GetHashCode(T obj)
{
return HashCode.Combine(obj.Parent, obj.ParentID, obj.Child, obj.ChildID);
}
}
} }

View File

@ -1,19 +1,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Library : IResource public class Library : IResource
{ {
[JsonIgnore] public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
public IEnumerable<string> Paths { get; set; } public string[] Paths { get; set; }
[EditableRelation] public virtual IEnumerable<ProviderID> Providers { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<ProviderID> Providers { get; set; }
[JsonIgnore] public virtual IEnumerable<Show> Shows { get; set; } [LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[JsonIgnore] public virtual IEnumerable<Collection> Collections { get; set; } [LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, ProviderID>> ProviderLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
#endif
public Library() { } public Library() { }
@ -21,8 +28,8 @@ namespace Kyoo.Models
{ {
Slug = slug; Slug = slug;
Name = name; Name = name;
Paths = paths; Paths = paths?.ToArray();
Providers = providers; Providers = providers?.ToArray();
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
@ -8,10 +9,10 @@ namespace Kyoo.Models
public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Poster { get; set; } [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; }
[EditableRelation] public virtual IEnumerable<MetadataID> ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [JsonReadOnly] public virtual IEnumerable<PeopleRole> Roles { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<PeopleRole> Roles { get; set; }
public People() {} public People() {}
@ -20,7 +21,7 @@ namespace Kyoo.Models
Slug = slug; Slug = slug;
Name = name; Name = name;
Poster = poster; Poster = poster;
ExternalIDs = externalIDs; ExternalIDs = externalIDs?.ToArray();
} }
} }
} }

View File

@ -1,13 +1,21 @@
using System.Collections.Generic;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class ProviderID : IResource public class ProviderID : IResource
{ {
[JsonIgnore] public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Logo { get; set; } [SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, ProviderID>> LibraryLinks { get; set; }
[SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; }
#endif
public ProviderID() { } public ProviderID() { }

View File

@ -1,25 +1,27 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Season : IResource public class Season : IResource
{ {
[JsonIgnore] public int ID { get; set; } public int ID { get; set; }
[JsonIgnore] public int ShowID { get; set; } public string Slug => $"{ShowSlug}-s{SeasonNumber}";
[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.Slug}-s{SeasonNumber}";
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; }
[JsonIgnore] public string Poster { get; set; } [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; }
[EditableRelation] public virtual IEnumerable<MetadataID> ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[JsonIgnore] public virtual Show Show { get; set; } [LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
[JsonIgnore] public virtual IEnumerable<Episode> Episodes { get; set; }
public Season() { } public Season() { }
@ -37,7 +39,7 @@ namespace Kyoo.Models
Overview = overview; Overview = overview;
Year = year; Year = year;
Poster = poster; Poster = poster;
ExternalIDs = externalIDs; ExternalIDs = externalIDs?.ToArray();
} }
} }
} }

View File

@ -9,8 +9,8 @@ namespace Kyoo.Models
public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Title { get; set; } public string Title { get; set; }
[EditableRelation] public IEnumerable<string> Aliases { get; set; } [EditableRelation] public string[] Aliases { get; set; }
[JsonIgnore] public string Path { get; set; } [SerializeIgnore] public string Path { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
public Status? Status { get; set; } public Status? Status { get; set; }
public string TrailerUrl { get; set; } public string TrailerUrl { get; set; }
@ -18,23 +18,30 @@ namespace Kyoo.Models
public int? StartYear { get; set; } public int? StartYear { get; set; }
public int? EndYear { get; set; } public int? EndYear { get; set; }
public string Poster { get; set; } [SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; }
public string Logo { get; set; } [SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; }
public string Backdrop { get; set; } [SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; }
public bool IsMovie { get; set; } public bool IsMovie { get; set; }
[EditableRelation] public virtual IEnumerable<MetadataID> ExternalIDs { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[JsonIgnore] public int? StudioID { get; set; } [SerializeIgnore] public int? StudioID { get; set; }
[EditableRelation] [JsonReadOnly] public virtual Studio Studio { get; set; } [LoadableRelation(nameof(StudioID))] [EditableRelation] public virtual Studio Studio { get; set; }
[EditableRelation] [JsonReadOnly] public virtual IEnumerable<Genre> Genres { get; set; } [LoadableRelation] [EditableRelation] public virtual ICollection<Genre> Genres { get; set; }
[EditableRelation] [JsonReadOnly] public virtual IEnumerable<PeopleRole> People { get; set; } [LoadableRelation] [EditableRelation] public virtual ICollection<PeopleRole> People { get; set; }
[JsonIgnore] public virtual IEnumerable<Season> Seasons { get; set; } [LoadableRelation] public virtual ICollection<Season> Seasons { get; set; }
[JsonIgnore] public virtual IEnumerable<Episode> Episodes { get; set; } [LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
[JsonIgnore] public virtual IEnumerable<Library> Libraries { get; set; } [LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
[JsonIgnore] public virtual IEnumerable<Collection> Collections { get; set; } [LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> LibraryLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> CollectionLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> GenreLinks { get; set; }
#endif
public Show() { } public Show() { }
@ -51,15 +58,15 @@ namespace Kyoo.Models
{ {
Slug = slug; Slug = slug;
Title = title; Title = title;
Aliases = aliases; Aliases = aliases?.ToArray();
Path = path; Path = path;
Overview = overview; Overview = overview;
TrailerUrl = trailerUrl; TrailerUrl = trailerUrl;
Genres = genres; Genres = genres?.ToArray();
Status = status; Status = status;
StartYear = startYear; StartYear = startYear;
EndYear = endYear; EndYear = endYear;
ExternalIDs = externalIDs; ExternalIDs = externalIDs?.ToArray();
} }
public Show(string slug, public Show(string slug,
@ -78,7 +85,7 @@ namespace Kyoo.Models
{ {
Slug = slug; Slug = slug;
Title = title; Title = title;
Aliases = aliases; Aliases = aliases?.ToArray();
Path = path; Path = path;
Overview = overview; Overview = overview;
TrailerUrl = trailerUrl; TrailerUrl = trailerUrl;
@ -88,7 +95,7 @@ namespace Kyoo.Models
Poster = poster; Poster = poster;
Logo = logo; Logo = logo;
Backdrop = backdrop; Backdrop = backdrop;
ExternalIDs = externalIDs; ExternalIDs = externalIDs?.ToArray();
} }
public string GetID(string provider) public string GetID(string provider)

View File

@ -5,11 +5,11 @@ namespace Kyoo.Models
{ {
public class Studio : IResource public class Studio : IResource
{ {
[JsonIgnore] public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public string Name { get; set; } public string Name { get; set; }
[JsonIgnore] public virtual IEnumerable<Show> Shows { get; set; } [LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
public Studio() { } public Studio() { }

View File

@ -12,7 +12,7 @@ namespace Kyoo.Models
Video = 1, Video = 1,
Audio = 2, Audio = 2,
Subtitle = 3, Subtitle = 3,
Font = 4 Attachment = 4
} }
namespace Watch namespace Watch
@ -25,8 +25,8 @@ namespace Kyoo.Models
public string Codec { get; set; } public string Codec { get; set; }
[MarshalAs(UnmanagedType.I1)] public bool isDefault; [MarshalAs(UnmanagedType.I1)] public bool isDefault;
[MarshalAs(UnmanagedType.I1)] public bool isForced; [MarshalAs(UnmanagedType.I1)] public bool isForced;
[JsonIgnore] public string Path { get; set; } [SerializeIgnore] public string Path { get; set; }
[JsonIgnore] public StreamType Type { get; set; } [SerializeIgnore] public StreamType Type { get; set; }
public Stream() {} public Stream() {}
@ -56,8 +56,9 @@ namespace Kyoo.Models
public class Track : Stream, IResource public class Track : Stream, IResource
{ {
[JsonIgnore] public int ID { get; set; } public int ID { get; set; }
[JsonIgnore] public int EpisodeID { get; set; } [SerializeIgnore] public int EpisodeID { get; set; }
public int TrackIndex { get; set; }
public bool IsDefault public bool IsDefault
{ {
get => isDefault; get => isDefault;
@ -76,7 +77,7 @@ namespace Kyoo.Models
string language = GetLanguage(Language); string language = GetLanguage(Language);
if (language == null) if (language == null)
return $"Unknown Language (id: {ID.ToString()})"; return $"Unknown (index: {TrackIndex})";
CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures) CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures)
.FirstOrDefault(x => x.ThreeLetterISOLanguageName == language); .FirstOrDefault(x => x.ThreeLetterISOLanguageName == language);
string name = info?.EnglishName ?? language; string name = info?.EnglishName ?? language;
@ -94,31 +95,37 @@ namespace Kyoo.Models
{ {
get get
{ {
if (Type != StreamType.Subtitle) string type = Type switch
return null;
string slug = string.IsNullOrEmpty(Language)
? ID.ToString()
: $"{Episode.Slug}.{Language}{(IsForced ? "-forced" : "")}";
switch (Codec)
{ {
case "ass": StreamType.Subtitle => "",
slug += ".ass"; StreamType.Video => "video.",
break; StreamType.Audio => "audio.",
case "subrip": StreamType.Attachment => "font.",
slug += ".srt"; _ => ""
break; };
} string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
return slug; string codec = Codec switch
{
"subrip" => ".srt",
{} x => $".{x}"
};
return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}";
} }
} }
[JsonIgnore] public bool IsExternal { get; set; } public bool IsExternal { get; set; }
[JsonIgnore] public virtual Episode Episode { get; set; } [LoadableRelation(nameof(EpisodeID))] public virtual Episode Episode { get; set; }
public Track() { } public Track() { }
public Track(StreamType type, string title, string language, bool isDefault, bool isForced, string codec, bool isExternal, string path) public Track(StreamType type,
string title,
string language,
bool isDefault,
bool isForced,
string codec,
bool isExternal,
string path)
: base(title, language, codec, isDefault, isForced, path, type) : base(title, language, codec, isDefault, isForced, path, type)
{ {
IsExternal = isExternal; IsExternal = isExternal;
@ -136,6 +143,7 @@ namespace Kyoo.Models
return mkvLanguage switch return mkvLanguage switch
{ {
"fre" => "fra", "fre" => "fra",
null => "und",
_ => mkvLanguage _ => mkvLanguage
}; };
} }

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
using Kyoo.Models.Watch;
using PathIO = System.IO.Path; using PathIO = System.IO.Path;
namespace Kyoo.Models namespace Kyoo.Models
@ -26,31 +25,34 @@ namespace Kyoo.Models
public class WatchItem public class WatchItem
{ {
[JsonIgnore] public readonly int EpisodeID = -1; public int EpisodeID { get; set; }
public string ShowTitle; public string ShowTitle { get; set; }
public string ShowSlug; public string ShowSlug { get; set; }
public int SeasonNumber; public int SeasonNumber { get; set; }
public int EpisodeNumber; public int EpisodeNumber { get; set; }
public string Title; public string Title { get; set; }
public string Slug; public string Slug { get; set; }
public DateTime? ReleaseDate; public DateTime? ReleaseDate { get; set; }
[JsonIgnore] public string Path; [SerializeIgnore] public string Path { get; set; }
public Episode PreviousEpisode; public Episode PreviousEpisode { get; set; }
public Episode NextEpisode; public Episode NextEpisode { get; set; }
public bool IsMovie; public bool IsMovie { get; set; }
public string Container; [SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; }
public Track Video; [SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; }
public IEnumerable<Track> Audios; [SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; }
public IEnumerable<Track> Subtitles;
public IEnumerable<Chapter> Chapters; public string Container { get; set; }
public Track Video { get; set; }
public ICollection<Track> Audios { get; set; }
public ICollection<Track> Subtitles { get; set; }
public ICollection<Chapter> Chapters { get; set; }
public WatchItem() { } public WatchItem() { }
private WatchItem(int episodeID, private WatchItem(int episodeID,
string showTitle, Show show,
string showSlug,
int seasonNumber, int seasonNumber,
int episodeNumber, int episodeNumber,
string title, string title,
@ -58,30 +60,34 @@ namespace Kyoo.Models
string path) string path)
{ {
EpisodeID = episodeID; EpisodeID = episodeID;
ShowTitle = showTitle; ShowTitle = show.Title;
ShowSlug = showSlug; ShowSlug = show.Slug;
SeasonNumber = seasonNumber; SeasonNumber = seasonNumber;
EpisodeNumber = episodeNumber; EpisodeNumber = episodeNumber;
Title = title; Title = title;
ReleaseDate = releaseDate; ReleaseDate = releaseDate;
Path = path; Path = path;
IsMovie = show.IsMovie;
Poster = show.Poster;
Logo = show.Logo;
Backdrop = show.Backdrop;
Container = Path.Substring(Path.LastIndexOf('.') + 1); Container = Path.Substring(Path.LastIndexOf('.') + 1);
Slug = Episode.GetSlug(ShowSlug, seasonNumber, episodeNumber); Slug = Episode.GetSlug(ShowSlug, seasonNumber, episodeNumber);
} }
private WatchItem(int episodeID, private WatchItem(int episodeID,
string showTitle, Show show,
string showSlug,
int seasonNumber, int seasonNumber,
int episodeNumber, int episodeNumber,
string title, string title,
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, show, seasonNumber, episodeNumber, title, releaseDate, path)
{ {
Video = video; Video = video;
Audios = audios; Audios = audios;
@ -90,11 +96,13 @@ namespace Kyoo.Models
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library) public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library)
{ {
Show show = await library.GetShow(ep.ShowID); // TODO load only the title, the slug & the IsMovie with the library manager.
Episode previous = null; Episode previous = null;
Episode next = null; Episode next = null;
if (!show.IsMovie) await library.Load(ep, x => x.Show);
await library.Load(ep, x => x.Tracks);
if (!ep.Show.IsMovie)
{ {
if (ep.EpisodeNumber > 1) if (ep.EpisodeNumber > 1)
previous = await library.GetEpisode(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); previous = await library.GetEpisode(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1);
@ -112,25 +120,23 @@ namespace Kyoo.Models
} }
return new WatchItem(ep.ID, return new WatchItem(ep.ID,
show.Title, ep.Show,
show.Slug,
ep.SeasonNumber, ep.SeasonNumber,
ep.EpisodeNumber, ep.EpisodeNumber,
ep.Title, ep.Title,
ep.ReleaseDate, ep.ReleaseDate,
ep.Path, ep.Path,
await library.GetTrack(x => x.EpisodeID == ep.ID && x.Type == StreamType.Video), ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
await library.GetTracks(x => x.EpisodeID == ep.ID && x.Type == StreamType.Audio), ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
await library.GetTracks(x => x.EpisodeID == ep.ID && x.Type == StreamType.Subtitle)) ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray())
{ {
IsMovie = show.IsMovie,
PreviousEpisode = previous, PreviousEpisode = previous,
NextEpisode = next, NextEpisode = next,
Chapters = await GetChapters(ep.Path) Chapters = await GetChapters(ep.Path)
}; };
} }
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)!,
@ -138,7 +144,7 @@ namespace Kyoo.Models
PathIO.GetFileNameWithoutExtension(episodePath) + ".txt" PathIO.GetFileNameWithoutExtension(episodePath) + ".txt"
); );
if (!File.Exists(path)) if (!File.Exists(path))
return new Chapter[0]; return Array.Empty<Chapter>();
try try
{ {
return (await File.ReadAllLinesAsync(path)) return (await File.ReadAllLinesAsync(path))
@ -152,7 +158,7 @@ namespace Kyoo.Models
catch catch
{ {
await Console.Error.WriteLineAsync($"Invalid chapter file at {path}"); await Console.Error.WriteLineAsync($"Invalid chapter file at {path}");
return new Chapter[0]; return Array.Empty<Chapter>();
} }
} }
} }

View File

@ -17,6 +17,37 @@ namespace Kyoo
{ {
public static class Utility public static class Utility
{ {
public static bool IsPropertyExpression(LambdaExpression ex)
{
return ex == null ||
ex.Body is MemberExpression ||
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
}
public static string GetPropertyName(LambdaExpression ex)
{
if (!IsPropertyExpression(ex))
throw new ArgumentException($"{ex} is not a property expression.");
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
? ((UnaryExpression)ex.Body).Operand as MemberExpression
: ex.Body as MemberExpression;
return member!.Member.Name;
}
public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj)
{
if (member == null)
throw new ArgumentNullException(nameof(member));
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return member switch
{
PropertyInfo property => property.GetValue(obj),
FieldInfo field => field.GetValue(obj),
_ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).")
};
}
public static string ToSlug(string str) public static string ToSlug(string str)
{ {
if (str == null) if (str == null)
@ -25,7 +56,7 @@ namespace Kyoo
str = str.ToLowerInvariant(); str = str.ToLowerInvariant();
string normalizedString = str.Normalize(NormalizationForm.FormD); string normalizedString = str.Normalize(NormalizationForm.FormD);
StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new();
foreach (char c in normalizedString) foreach (char c in normalizedString)
{ {
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
@ -93,7 +124,7 @@ namespace Kyoo
return first; return first;
} }
public static T Complete<T>(T first, T second) public static T Complete<T>(T first, T second, Func<PropertyInfo, bool> where = null)
{ {
if (first == null) if (first == null)
throw new ArgumentNullException(nameof(first)); throw new ArgumentNullException(nameof(first));
@ -105,6 +136,9 @@ namespace Kyoo
.Where(x => x.CanRead && x.CanWrite .Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null); && Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties) foreach (PropertyInfo property in properties)
{ {
object value = property.GetValue(second); object value = property.GetValue(second);
@ -112,7 +146,7 @@ namespace Kyoo
? Activator.CreateInstance(property.PropertyType) ? Activator.CreateInstance(property.PropertyType)
: null; : null;
if (value?.Equals(defaultValue) == false) if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
property.SetValue(first, value); property.SetValue(first, value);
} }
@ -222,6 +256,154 @@ namespace Kyoo
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
} }
public static IEnumerable<T2> Map<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, int, T2> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return mapper(enumerator.Current, index);
index++;
}
}
public static async IAsyncEnumerable<T2> MapAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, int, Task<T2>> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return await mapper(enumerator.Current, index);
index++;
}
}
public static async IAsyncEnumerable<T2> SelectAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, Task<T2>> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
while (enumerator.MoveNext())
yield return await mapper(enumerator.Current);
}
public static async Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
List<T> ret = new();
await foreach(T i in self)
ret.Add(i);
return ret;
}
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
if (!enumerator.MoveNext())
{
action();
yield break;
}
do
{
yield return enumerator.Current;
}
while (enumerator.MoveNext());
}
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
{
if (self == null)
return;
foreach (T i in self)
action(i);
}
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
{
if (self == null)
return;
foreach (object i in self)
action(i);
}
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
{
if (self == null)
return;
foreach (T i in self)
await action(i);
}
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
{
if (self == null)
return;
await foreach (T i in self)
action(i);
}
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
{
if (self == null)
return;
foreach (object i in self)
await action(i);
}
private static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
{
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length)
.Where(x => x.GetParameters().Length == args.Length)
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
$"{args.Length} arguments and {generics.Length} generic " +
$"types could not be found on {type.Name}."))
.Where(x =>
{
int i = 0;
return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
})
.IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
.Where(x =>
{
int i = 0;
return x.GetParameters().All(y => y.ParameterType == args[i++].GetType());
})
.IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
.Take(2)
.ToArray();
if (methods.Length == 1)
return methods[0];
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
}
public static T RunGenericMethod<T>( public static T RunGenericMethod<T>(
[NotNull] Type owner, [NotNull] Type owner,
[NotNull] string methodName, [NotNull] string methodName,
@ -245,10 +427,7 @@ namespace Kyoo
throw new ArgumentNullException(nameof(types)); throw new ArgumentNullException(nameof(types));
if (types.Length < 1) if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = owner.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
.SingleOrDefault(x => x.Name == methodName && x.GetParameters().Length == args.Length);
if (method == null)
throw new NullReferenceException($"A method named {methodName} with {args.Length} arguments could not be found on {owner.FullName}");
return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray()); return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
} }
@ -257,17 +436,24 @@ namespace Kyoo
[NotNull] string methodName, [NotNull] string methodName,
[NotNull] Type type, [NotNull] Type type,
params object[] args) params object[] args)
{
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
}
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
[NotNull] Type[] types,
params object[] args)
{ {
if (instance == null) if (instance == null)
throw new ArgumentNullException(nameof(instance)); throw new ArgumentNullException(nameof(instance));
if (methodName == null) if (methodName == null)
throw new ArgumentNullException(nameof(methodName)); throw new ArgumentNullException(nameof(methodName));
if (type == null) if (types == null || types.Length == 0)
throw new ArgumentNullException(nameof(type)); throw new ArgumentNullException(nameof(types));
MethodInfo method = instance.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
if (method == null) return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
throw new NullReferenceException($"A method named {methodName} could not be found on {instance.GetType().FullName}");
return (T)method.MakeGenericMethod(type).Invoke(instance, args?.ToArray());
} }
[NotNull] [NotNull]
@ -369,6 +555,22 @@ namespace Kyoo
}, TaskContinuationOptions.ExecuteSynchronously); }, TaskContinuationOptions.ExecuteSynchronously);
} }
public static Expression<Func<T, bool>> ResourceEquals<T>(IResource obj)
where T : IResource
{
if (obj.ID > 0)
return x => x.ID == obj.ID || x.Slug == obj.Slug;
return x => x.Slug == obj.Slug;
}
public static Func<T, bool> ResourceEqualsFunc<T>(IResource obj)
where T : IResource
{
if (obj.ID > 0)
return x => x.ID == obj.ID || x.Slug == obj.Slug;
return x => x.Slug == obj.Slug;
}
public static bool ResourceEquals([CanBeNull] object first, [CanBeNull] object second) public static bool ResourceEquals([CanBeNull] object first, [CanBeNull] object second)
{ {
if (ReferenceEquals(first, second)) if (ReferenceEquals(first, second))
@ -382,11 +584,7 @@ namespace Kyoo
Type type = GetEnumerableType(eno); Type type = GetEnumerableType(eno);
if (typeof(IResource).IsAssignableFrom(type)) if (typeof(IResource).IsAssignableFrom(type))
return ResourceEquals(eno.Cast<IResource>(), ens.Cast<IResource>()); return ResourceEquals(eno.Cast<IResource>(), ens.Cast<IResource>());
Type genericDefinition = GetGenericDefinition(type, typeof(IResourceLink<,>));
if (genericDefinition == null)
return RunGenericMethod<bool>(typeof(Enumerable), "SequenceEqual", type, first, second); return RunGenericMethod<bool>(typeof(Enumerable), "SequenceEqual", type, first, second);
Type[] types = genericDefinition.GetGenericArguments().Prepend(type).ToArray();
return RunGenericMethod<bool>(typeof(Utility), "LinkEquals", types, eno, ens);
} }
public static bool ResourceEquals<T>([CanBeNull] T first, [CanBeNull] T second) public static bool ResourceEquals<T>([CanBeNull] T first, [CanBeNull] T second)
@ -422,69 +620,5 @@ namespace Kyoo
return true; return true;
return firstID == secondID; return firstID == secondID;
} }
public static bool LinkEquals<T, T1, T2>([CanBeNull] IEnumerable<T> first, [CanBeNull] IEnumerable<T> second)
where T : IResourceLink<T1, T2>
where T1 : IResource
where T2 : IResource
{
if (ReferenceEquals(first, second))
return true;
if (first == null || second == null)
return false;
return first.SequenceEqual(second, new LinkComparer<T, T1, T2>());
}
public static Expression<T> Convert<T>([CanBeNull] this Expression expr)
where T : Delegate
{
Expression<T> e = expr switch
{
null => null,
LambdaExpression lambda => new ExpressionConverter<T>(lambda).VisitAndConvert(),
_ => throw new ArgumentException("Can't convert a non lambda.")
};
return ExpressionRewrite.Rewrite<T>(e);
}
private class ExpressionConverter<TTo> : ExpressionVisitor
where TTo : Delegate
{
private readonly LambdaExpression _expression;
private readonly ParameterExpression[] _newParams;
internal ExpressionConverter(LambdaExpression expression)
{
_expression = expression;
Type[] paramTypes = typeof(TTo).GetGenericArguments()[..^1];
if (paramTypes.Length != _expression.Parameters.Count)
throw new ArgumentException("Parameter count from internal and external lambda are not matched.");
_newParams = new ParameterExpression[paramTypes.Length];
for (int i = 0; i < paramTypes.Length; i++)
{
if (_expression.Parameters[i].Type == paramTypes[i])
_newParams[i] = _expression.Parameters[i];
else
_newParams[i] = Expression.Parameter(paramTypes[i], _expression.Parameters[i].Name);
}
}
internal Expression<TTo> VisitAndConvert()
{
Type returnType = _expression.Type.GetGenericArguments().Last();
Expression body = _expression.ReturnType == returnType
? Visit(_expression.Body)
: Expression.Convert(Visit(_expression.Body)!, returnType);
return Expression.Lambda<TTo>(body!, _newParams);
}
protected override Expression VisitParameter(ParameterExpression node)
{
return _newParams.FirstOrDefault(x => x.Name == node.Name) ?? node;
}
}
} }
} }

View File

@ -26,12 +26,7 @@ namespace Kyoo.CommonApi
Expression<Func<T, bool>> defaultWhere = null) Expression<Func<T, bool>> defaultWhere = null)
{ {
if (where == null || where.Count == 0) if (where == null || where.Count == 0)
{ return defaultWhere;
if (defaultWhere == null)
return null;
Expression body = ExpressionRewrite.Rewrite(defaultWhere.Body);
return Expression.Lambda<Func<T, bool>>(body, defaultWhere.Parameters.First());
}
ParameterExpression param = defaultWhere?.Parameters.First() ?? Expression.Parameter(typeof(T)); ParameterExpression param = defaultWhere?.Parameters.First() ?? Expression.Parameter(typeof(T));
Expression expression = defaultWhere?.Body; Expression expression = defaultWhere?.Body;
@ -97,8 +92,7 @@ namespace Kyoo.CommonApi
expression = condition; expression = condition;
} }
expression = ExpressionRewrite.Rewrite(expression); return Expression.Lambda<Func<T, bool>>(expression!, param);
return Expression.Lambda<Func<T, bool>>(expression, param);
} }
private static Expression ResourceEqual(Expression parameter, string value, bool notEqual = false) private static Expression ResourceEqual(Expression parameter, string value, bool notEqual = false)

View File

@ -12,6 +12,7 @@ using Microsoft.Extensions.Configuration;
namespace Kyoo.CommonApi namespace Kyoo.CommonApi
{ {
[ApiController] [ApiController]
[ResourceView]
public class CrudApi<T> : ControllerBase where T : class, IResource public class CrudApi<T> : ControllerBase where T : class, IResource
{ {
private readonly IRepository<T> _repository; private readonly IRepository<T> _repository;
@ -23,9 +24,9 @@ namespace Kyoo.CommonApi
BaseURL = configuration.GetValue<string>("public_url").TrimEnd('/'); BaseURL = configuration.GetValue<string>("public_url").TrimEnd('/');
} }
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
[JsonDetailed]
public virtual async Task<ActionResult<T>> Get(int id) public virtual async Task<ActionResult<T>> Get(int id)
{ {
T resource = await _repository.Get(id); T resource = await _repository.Get(id);
@ -37,7 +38,6 @@ namespace Kyoo.CommonApi
[HttpGet("{slug}")] [HttpGet("{slug}")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
[JsonDetailed]
public virtual async Task<ActionResult<T>> Get(string slug) public virtual async Task<ActionResult<T>> Get(string slug)
{ {
T resource = await _repository.Get(slug); T resource = await _repository.Get(slug);
@ -68,10 +68,6 @@ namespace Kyoo.CommonApi
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<T> resources = await _repository.GetAll(ApiHelper.ParseWhere<T>(where), ICollection<T> resources = await _repository.GetAll(ApiHelper.ParseWhere<T>(where),
@ -89,7 +85,7 @@ namespace Kyoo.CommonApi
protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit) protected Page<TResult> Page<TResult>(ICollection<TResult> resources, int limit)
where TResult : IResource where TResult : IResource
{ {
return new Page<TResult>(resources, return new(resources,
BaseURL + Request.Path, BaseURL + Request.Path,
Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase),
limit); limit);
@ -186,5 +182,20 @@ namespace Kyoo.CommonApi
return Ok(); return Ok();
} }
[Authorize(Policy = "Write")]
public virtual async Task<IActionResult> Delete(Dictionary<string, string> where)
{
try
{
await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where));
}
catch (ItemNotFound)
{
return NotFound();
}
return Ok();
}
} }
} }

View File

@ -1,103 +1,138 @@
using System; using System;
using System.Buffers; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver
{ {
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) private int _depth = -1;
{ private string _host;
JsonProperty property = base.CreateProperty(member, memberSerialization);
property.ShouldSerialize = i => member.GetCustomAttribute<JsonReadOnly>(true) == null; public JsonPropertyIgnorer(string host)
property.ShouldDeserialize = i => member.GetCustomAttribute<JsonIgnore>(true) == null;
return property;
}
}
public class JsonPropertySelector : JsonPropertyIgnorer
{ {
private readonly Dictionary<Type, HashSet<string>> _ignored; _host = host;
private readonly Dictionary<Type, HashSet<string>> _forceSerialize;
public JsonPropertySelector()
{
_ignored = new Dictionary<Type, HashSet<string>>();
_forceSerialize = new Dictionary<Type, HashSet<string>>();
}
public JsonPropertySelector(Dictionary<Type, HashSet<string>> ignored,
Dictionary<Type, HashSet<string>> forceSerialize = null)
{
_ignored = ignored ?? new Dictionary<Type, HashSet<string>>();
_forceSerialize = forceSerialize ?? new Dictionary<Type, HashSet<string>>();
}
private bool IsIgnored(Type type, string propertyName)
{
while (type != null)
{
if (_ignored.ContainsKey(type) && _ignored[type].Contains(propertyName))
return true;
type = type.BaseType;
}
return false;
}
private bool IsSerializationForced(Type type, string propertyName)
{
while (type != null)
{
if (_forceSerialize.ContainsKey(type) && _forceSerialize[type].Contains(propertyName))
return true;
type = type.BaseType;
}
return false;
} }
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{ {
JsonProperty property = base.CreateProperty(member, memberSerialization); JsonProperty property = base.CreateProperty(member, memberSerialization);
if (IsSerializationForced(property.DeclaringType, property.PropertyName)) LoadableRelationAttribute relation = member?.GetCustomAttribute<LoadableRelationAttribute>();
property.ShouldSerialize = i => true; if (relation != null)
else if (IsIgnored(property.DeclaringType, property.PropertyName)) {
property.ShouldSerialize = i => false; if (relation.RelationID == null)
property.ShouldSerialize = x => _depth == 0 && member.GetValue(x) != null;
else
property.ShouldSerialize = x =>
{
if (_depth != 0)
return false;
if (member.GetValue(x) != null)
return true;
return x.GetType().GetProperty(relation.RelationID)?.GetValue(x) != null;
};
}
if (member?.GetCustomAttribute<SerializeIgnoreAttribute>() != null)
property.ShouldSerialize = _ => false;
if (member?.GetCustomAttribute<DeserializeIgnoreAttribute>() != null)
property.ShouldDeserialize = _ => false;
SerializeAsAttribute serializeAs = member?.GetCustomAttribute<SerializeAsAttribute>();
if (serializeAs != null)
property.ValueProvider = new SerializeAsProvider(serializeAs.Format, _host);
return property; return property;
} }
protected override JsonContract CreateContract(Type objectType)
{
JsonContract contract = base.CreateContract(objectType);
if (Utility.GetGenericDefinition(objectType, typeof(Page<>)) == null
&& !objectType.IsAssignableTo(typeof(IEnumerable)))
{
contract.OnSerializingCallbacks.Add((_, _) => _depth++);
contract.OnSerializedCallbacks.Add((_, _) => _depth--);
}
return contract;
}
} }
public class JsonDetailed : ActionFilterAttribute public class PeopleRoleConverter : JsonConverter<PeopleRole>
{ {
public override void OnActionExecuted(ActionExecutedContext context) public override void WriteJson(JsonWriter writer, PeopleRole value, JsonSerializer serializer)
{ {
if (context.Result is ObjectResult result) ICollection<PeopleRole> oldPeople = value.Show?.People;
{ ICollection<PeopleRole> oldRoles = value.People?.Roles;
result.Formatters.Add(new NewtonsoftJsonOutputFormatter( if (value.Show != null)
new JsonSerializerSettings value.Show.People = null;
{ if (value.People != null)
ContractResolver = new JsonPropertySelector(null, new Dictionary<Type, HashSet<string>> value.People.Roles = null;
{
{typeof(Show), new HashSet<string> {"genres", "studio"}}, JObject obj = JObject.FromObject(value.ForPeople ? value.People : value.Show, serializer);
{typeof(Episode), new HashSet<string> {"tracks"}}, obj.Add("role", value.Role);
{typeof(PeopleRole), new HashSet<string> {"show"}} obj.Add("type", value.Type);
}) obj.WriteTo(writer);
},
context.HttpContext.RequestServices.GetRequiredService<ArrayPool<char>>(), if (value.Show != null)
new MvcOptions())); value.Show.People = oldPeople;
if (value.People != null)
value.People.Roles = oldRoles;
} }
public override PeopleRole ReadJson(JsonReader reader,
Type objectType,
PeopleRole existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class SerializeAsProvider : IValueProvider
{
private string _format;
private string _host;
public SerializeAsProvider(string format, string host)
{
_format = format;
_host = host.TrimEnd('/');
}
public object GetValue(object target)
{
return Regex.Replace(_format, @"(?<!{){(\w+)}", x =>
{
string value = x.Groups[1].Value;
if (value == "HOST")
return _host;
PropertyInfo properties = target.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.FirstOrDefault(y => y.Name == value);
if (properties == null)
return null;
if (properties.GetValue(target) is string ret)
return ret;
throw new ArgumentException($"Invalid serializer replacement {value}");
});
}
public void SetValue(object target, object value)
{
// Values are ignored and should not be editable, except if the internal value is set.
} }
} }
} }

View File

@ -12,10 +12,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="4.1.3" /> <PackageReference Include="Npgsql" Version="5.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,22 +1,18 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public abstract class LocalRepository<T> public abstract class LocalRepository<T> : IRepository<T>
where T : class, IResource where T : class, IResource
{ {
protected readonly DbContext Database; protected readonly DbContext Database;
@ -32,6 +28,7 @@ namespace Kyoo.Controllers
public virtual void Dispose() public virtual void Dispose()
{ {
Database.Dispose(); Database.Dispose();
GC.SuppressFinalize(this);
} }
public virtual ValueTask DisposeAsync() public virtual ValueTask DisposeAsync()
@ -44,6 +41,11 @@ namespace Kyoo.Controllers
return Database.Set<T>().FirstOrDefaultAsync(x => x.ID == id); return Database.Set<T>().FirstOrDefaultAsync(x => x.ID == id);
} }
public virtual Task<T> GetWithTracking(int id)
{
return Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
}
public virtual Task<T> Get(string slug) public virtual Task<T> Get(string slug)
{ {
return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug); return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug);
@ -54,6 +56,8 @@ namespace Kyoo.Controllers
return Database.Set<T>().FirstOrDefaultAsync(predicate); return Database.Set<T>().FirstOrDefaultAsync(predicate);
} }
public abstract Task<ICollection<T>> Search(string query);
public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null, public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
Sort<T> sort = default, Sort<T> sort = default,
Pagination limit = default) Pagination limit = default)
@ -112,7 +116,7 @@ namespace Kyoo.Controllers
return query.CountAsync(); return query.CountAsync();
} }
public virtual async Task<T> Create([NotNull] T obj) public virtual async Task<T> Create(T obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
@ -153,53 +157,30 @@ namespace Kyoo.Controllers
if (edited == null) if (edited == null)
throw new ArgumentNullException(nameof(edited)); throw new ArgumentNullException(nameof(edited));
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
Database.ChangeTracker.LazyLoadingEnabled = false; Database.ChangeTracker.LazyLoadingEnabled = false;
try try
{ {
T old = await Get(edited.ID); T old = await GetWithTracking(edited.ID);
if (old == null) if (old == null)
throw new ItemNotFound($"No resource found with the ID {edited.ID}."); throw new ItemNotFound($"No resource found with the ID {edited.ID}.");
foreach (NavigationEntry navigation in Database.Entry(old).Navigations)
{
if (navigation.Metadata.PropertyInfo.GetCustomAttribute<EditableRelation>() != null)
{
if (resetOld)
{
await navigation.LoadAsync();
continue;
}
IClrPropertyGetter getter = navigation.Metadata.GetGetter();
if (getter.HasDefaultValue(edited))
continue;
await navigation.LoadAsync();
// TODO this may be usless for lists since the API does not return IDs but the
// TODO LinkEquality does not check slugs (their are lazy loaded and only the ID is available)
if (Utility.ResourceEquals(getter.GetClrValue(edited), getter.GetClrValue(old)))
navigation.Metadata.PropertyInfo.SetValue(edited, default);
}
else
navigation.Metadata.PropertyInfo.SetValue(edited, default);
}
if (resetOld) if (resetOld)
Utility.Nullify(old); Utility.Nullify(old);
Utility.Complete(old, edited); Utility.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null);
await Validate(old); await EditRelations(old, edited, resetOld);
await Database.SaveChangesAsync(); await Database.SaveChangesAsync();
return old; return old;
} }
finally finally
{ {
Database.ChangeTracker.LazyLoadingEnabled = true; Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
} }
} }
protected bool ShouldValidate<T2>(T2 value) protected virtual Task EditRelations(T resource, T changed, bool resetOld)
{ {
return value != null && Database.Entry(value).State == EntityState.Detached; return Validate(resource);
} }
protected virtual Task Validate(T resource) protected virtual Task Validate(T resource)
@ -221,18 +202,6 @@ namespace Kyoo.Controllers
throw new ArgumentException("Resources slug can't be number only."); throw new ArgumentException("Resources slug can't be number only.");
} }
} }
foreach (PropertyInfo property in typeof(T).GetProperties()
.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)
&& !typeof(string).IsAssignableFrom(x.PropertyType)
&& x.GetCustomAttribute<EditableRelation>() != null))
{
object value = property.GetValue(resource);
if (value == null || value is ICollection || Utility.IsOfGenericType(value, typeof(ICollection<>)))
continue;
value = Utility.RunGenericMethod<object>(typeof(Enumerable), "ToList", Utility.GetEnumerableType((IEnumerable)value), value);
property.SetValue(resource, value);
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -267,113 +236,11 @@ namespace Kyoo.Controllers
foreach (string slug in slugs) foreach (string slug in slugs)
await Delete(slug); await Delete(slug);
} }
}
public abstract class LocalRepository<T, TInternal> : LocalRepository<TInternal>, IRepository<T> public async Task DeleteRange(Expression<Func<T, bool>> where)
where T : class, IResource
where TInternal : class, T, new()
{ {
protected LocalRepository(DbContext database) : base(database) { } ICollection<T> resources = await GetAll(where);
await DeleteRange(resources);
public new Task<T> Get(int id)
{
return base.Get(id).Cast<T>();
}
public new Task<T> Get(string slug)
{
return base.Get(slug).Cast<T>();
}
public Task<T> Get(Expression<Func<T, bool>> predicate)
{
return Get(predicate.Convert<Func<TInternal, bool>>()).Cast<T>();
}
public abstract Task<ICollection<T>> Search(string query);
public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
Pagination limit = default)
{
return ApplyFilters(Database.Set<TInternal>(), where, sort, limit);
}
protected virtual async Task<ICollection<T>> ApplyFilters(IQueryable<TInternal> query,
Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
Pagination limit = default)
{
ICollection<TInternal> items = await ApplyFilters(query,
base.Get,
DefaultSort,
where.Convert<Func<TInternal, bool>>(),
sort.To<TInternal>(),
limit);
return items.ToList<T>();
}
public virtual Task<int> GetCount(Expression<Func<T, bool>> where = null)
{
IQueryable<TInternal> query = Database.Set<TInternal>();
if (where != null)
query = query.Where(where.Convert<Func<TInternal, bool>>());
return query.CountAsync();
}
Task<T> IRepository<T>.Create(T item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
TInternal obj = item as TInternal ?? new TInternal();
if (!(item is TInternal))
Utility.Assign(obj, item);
return Create(obj).Cast<T>()
.Then(x => item.ID = x.ID);
}
Task<T> IRepository<T>.CreateIfNotExists(T item, bool silentFail)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
TInternal obj = item as TInternal ?? new TInternal();
if (!(item is TInternal))
Utility.Assign(obj, item);
return CreateIfNotExists(obj, silentFail).Cast<T>()
.Then(x => item.ID = x.ID);
}
public Task<T> Edit(T edited, bool resetOld)
{
if (edited == null)
throw new ArgumentNullException(nameof(edited));
if (edited is TInternal intern)
return Edit(intern, resetOld).Cast<T>();
TInternal obj = new TInternal();
Utility.Assign(obj, edited);
return base.Edit(obj, resetOld).Cast<T>();
}
public abstract override Task Delete([NotNull] TInternal obj);
Task IRepository<T>.Delete(T obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (obj is TInternal intern)
return Delete(intern);
TInternal item = new TInternal();
Utility.Assign(item, obj);
return Delete(item);
}
public virtual async Task DeleteRange(IEnumerable<T> objs)
{
foreach (T obj in objs)
await ((IRepository<T>)this).Delete(obj);
} }
} }
} }

View File

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Attributes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Kyoo.CommonApi
{
public class ResourceViewAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary<string, string> where)
{
where.Remove("fields");
foreach ((string key, _) in context.ActionArguments)
where.Remove(key);
}
List<string> fields = context.HttpContext.Request.Query["fields"]
.SelectMany(x => x.Split(','))
.ToList();
if (fields.Contains("internal"))
{
fields.Remove("internal");
context.HttpContext.Items["internal"] = true;
// TODO disable SerializeAs attributes when this is true.
}
if (context.ActionDescriptor is ControllerActionDescriptor descriptor)
{
Type type = descriptor.MethodInfo.ReturnType;
type = Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] ?? type;
type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type;
type = Utility.GetGenericDefinition(type, typeof(Page<>))?.GetGenericArguments()[0] ?? type;
PropertyInfo[] properties = type.GetProperties()
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
.ToArray();
fields = fields.Select(x =>
{
string property = properties
.FirstOrDefault(y => string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
?.Name;
if (property != null)
return property;
context.Result = new BadRequestObjectResult(new
{
Error = $"{x} does not exist on {type.Name}."
});
return null;
})
.ToList();
if (context.Result != null)
return;
}
context.HttpContext.Items["fields"] = fields;
base.OnActionExecuting(context);
}
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is ObjectResult result)
await LoadResultRelations(context, result);
await base.OnResultExecutionAsync(context, next);
}
private static async Task LoadResultRelations(ActionContext context, ObjectResult result)
{
if (result.DeclaredType == null)
return;
await using ILibraryManager library = context.HttpContext.RequestServices.GetService<ILibraryManager>();
ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"];
Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>));
if (pageType != null)
{
foreach (IResource resource in ((dynamic)result.Value).Items)
{
foreach (string field in fields!)
await library!.Load(resource, field);
}
}
else if (result.DeclaredType.IsAssignableTo(typeof(IResource)))
{
foreach (string field in fields!)
await library!.Load((IResource)result.Value, field);
}
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Controllers
{
public class FileManager : IFileManager
{
private FileExtensionContentTypeProvider _provider;
private string _GetContentType(string path)
{
if (_provider == null)
{
_provider = new FileExtensionContentTypeProvider();
_provider.Mappings[".mkv"] = "video/x-matroska";
_provider.Mappings[".ass"] = "text/x-ssa";
_provider.Mappings[".srt"] = "application/x-subrip";
}
if (_provider.TryGetContentType(path, out string contentType))
return contentType;
throw new NotImplementedException($"Can't get the content type of the file at: {path}");
}
// TODO add a way to force content type
public IActionResult FileResult(string path, bool range)
{
if (path == null)
return new NotFoundResult();
if (!File.Exists(path))
return new NotFoundResult();
return new PhysicalFileResult(Path.GetFullPath(path), _GetContentType(path))
{
EnableRangeProcessing = range
};
}
public StreamReader GetReader(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return new StreamReader(path);
}
public Task<ICollection<string>> ListFiles(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return Task.FromResult<ICollection<string>>(Directory.GetFiles(path));
}
public Task<bool> Exists(string path)
{
return Task.FromResult(File.Exists(path));
}
public string GetExtraDirectory(Show show)
{
string path = Path.Combine(show.Path, "Extra");
Directory.CreateDirectory(path);
return path;
}
public string GetExtraDirectory(Season season)
{
if (season.Show == null)
throw new NotImplementedException("Can't get season's extra directory when season.Show == null.");
// TODO use a season.Path here.
string path = Path.Combine(season.Show.Path, "Extra");
Directory.CreateDirectory(path);
return path;
}
public string GetExtraDirectory(Episode episode)
{
string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra");
Directory.CreateDirectory(path);
return path;
}
}
}

View File

@ -18,7 +18,7 @@ namespace Kyoo.Controllers
private async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what) private async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what)
where T : new() where T : new()
{ {
T ret = new T(); T ret = new();
IEnumerable<IMetadataProvider> providers = library?.Providers IEnumerable<IMetadataProvider> providers = library?.Providers
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug)) .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
@ -40,11 +40,11 @@ namespace Kyoo.Controllers
} }
private async Task<List<T>> GetMetadata<T>( private async Task<List<T>> GetMetadata<T>(
Func<IMetadataProvider, Task<IEnumerable<T>>> providerCall, Func<IMetadataProvider, Task<ICollection<T>>> providerCall,
Library library, Library library,
string what) string what)
{ {
List<T> ret = new List<T>(); List<T> ret = new();
IEnumerable<IMetadataProvider> providers = library?.Providers IEnumerable<IMetadataProvider> providers = library?.Providers
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug)) .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
@ -121,6 +121,7 @@ namespace Kyoo.Controllers
$"the season {seasonNumber} of {show.Title}"); $"the season {seasonNumber} of {show.Title}");
season.Show = show; season.Show = show;
season.ShowID = show.ID; season.ShowID = show.ID;
season.ShowSlug = show.Slug;
season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber; season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber;
season.Title ??= $"Season {season.SeasonNumber}"; season.Title ??= $"Season {season.SeasonNumber}";
return season; return season;
@ -139,6 +140,7 @@ namespace Kyoo.Controllers
"an episode"); "an episode");
episode.Show = show; episode.Show = show;
episode.ShowID = show.ID; episode.ShowID = show.ID;
episode.ShowSlug = show.Slug;
episode.Path = episodePath; episode.Path = episodePath;
episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber;
episode.EpisodeNumber = episode.EpisodeNumber != -1 ? episode.EpisodeNumber : episodeNumber; episode.EpisodeNumber = episode.EpisodeNumber != -1 ? episode.EpisodeNumber : episodeNumber;
@ -146,7 +148,7 @@ namespace Kyoo.Controllers
return episode; return episode;
} }
public async Task<IEnumerable<PeopleRole>> GetPeople(Show show, Library library) public async Task<ICollection<PeopleRole>> GetPeople(Show show, Library library)
{ {
List<PeopleRole> people = await GetMetadata( List<PeopleRole> people = await GetMetadata(
provider => provider.GetPeople(show), provider => provider.GetPeople(show),

View File

@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class CollectionRepository : LocalRepository<Collection, CollectionDE>, ICollectionRepository public class CollectionRepository : LocalRepository<Collection>, ICollectionRepository
{ {
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
protected override Expression<Func<CollectionDE, object>> DefaultSort => x => x.Name; protected override Expression<Func<Collection, object>> DefaultSort => x => x.Name;
public CollectionRepository(DatabaseContext database) : base(database) public CollectionRepository(DatabaseContext database) : base(database)
{ {
@ -25,6 +25,7 @@ namespace Kyoo.Controllers
return; return;
_disposed = true; _disposed = true;
_database.Dispose(); _database.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -39,11 +40,12 @@ namespace Kyoo.Controllers
{ {
return await _database.Collections return await _database.Collections
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync<Collection>(); .ToListAsync();
} }
public override async Task<CollectionDE> Create(CollectionDE obj) public override async Task<Collection> Create(Collection obj)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
@ -51,18 +53,12 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
public override async Task Delete(CollectionDE obj) public override async Task Delete(Collection obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
if (obj.Links != null)
foreach (CollectionLink link in obj.Links)
_database.Entry(link).State = EntityState.Deleted;
if (obj.LibraryLinks != null)
foreach (LibraryLink link in obj.LibraryLinks)
_database.Entry(link).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
} }

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,21 @@ 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 ITrackRepository _tracks;
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,
ITrackRepository tracks)
: base(database)
{ {
_database = database; _database = database;
_providers = providers; _providers = providers;
_shows = shows;
_tracks = tracks;
} }
@ -32,6 +39,8 @@ namespace Kyoo.Controllers
_disposed = true; _disposed = true;
_database.Dispose(); _database.Dispose();
_providers.Dispose(); _providers.Dispose();
_shows.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -41,11 +50,20 @@ 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)
{ {
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)-e(?<episode>\d*)"); Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)e(?<episode>\d*)");
if (!match.Success) if (!match.Success)
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug);
@ -54,75 +72,137 @@ namespace Kyoo.Controllers
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);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
public async Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber)
{
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 showID, int seasonNumber, int episodeNumber) public async Task<Episode> Get(int showID, int seasonNumber, int episodeNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber && x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber); && x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
} }
public Task<Episode> Get(int seasonID, int episodeNumber) public async Task<Episode> Get(int seasonID, int episodeNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID
&& x.EpisodeNumber == episodeNumber); && x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
} }
public Task<Episode> GetAbsolute(int showID, int absoluteNumber) public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID 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 Task<Episode> GetAbsolute(string showSlug, int absoluteNumber) public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber); && x.AbsoluteNumber == absoluteNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
} }
public 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}%") && x.EpisodeNumber != -1)
.OrderBy(DefaultSort)
.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)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
if (obj.ExternalIDs != null) obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
foreach (MetadataID entry in obj.ExternalIDs)
_database.Entry(entry).State = EntityState.Added;
if (obj.Tracks != null)
foreach (Track entry in obj.Tracks)
_database.Entry(entry).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
return obj; return await ValidateTracks(obj);
} }
protected override async Task Validate(Episode resource) protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
{ {
if (resource.ShowID <= 0) if (resource.ShowID <= 0)
throw new InvalidOperationException($"Can't store an episode not related to any show (showID: {resource.ShowID})."); throw new InvalidOperationException($"Can't store an episode not related to any show (showID: {resource.ShowID}).");
await base.Validate(resource); if (changed.Tracks != null || resetOld)
if (resource.ExternalIDs != null)
{ {
foreach (MetadataID link in resource.ExternalIDs) await _tracks.DeleteRange(x => x.EpisodeID == resource.ID);
if (ShouldValidate(link)) resource.Tracks = changed.Tracks;
link.Provider = await _providers.CreateIfNotExists(link.Provider, true); await ValidateTracks(resource);
} }
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
await Validate(resource);
}
private async Task<Episode> ValidateTracks(Episode resource)
{
resource.Tracks = await resource.Tracks.MapAsync((x, i) =>
{
x.Episode = resource;
x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language
&& x.IsForced == y.IsForced
&& x.Codec == y.Codec
&& x.Type == y.Type);
return _tracks.Create(x);
}).ToListAsync();
return resource;
}
protected override async Task Validate(Episode resource)
{
await base.Validate(resource);
resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x =>
{
x.Provider = await _providers.CreateIfNotExists(x.Provider, true);
x.ProviderID = x.Provider.ID;
_database.Entry(x.Provider).State = EntityState.Detached;
return x;
}).ToListAsync();
} }
public async Task Delete(string showSlug, int seasonNumber, int episodeNumber) public async Task Delete(string showSlug, int seasonNumber, int episodeNumber)
@ -137,10 +217,8 @@ namespace Kyoo.Controllers
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
if (obj.ExternalIDs != null) await obj.Tracks.ForEachAsync(x => _tracks.Delete(x));
foreach (MetadataID entry in obj.ExternalIDs) obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
_database.Entry(entry).State = EntityState.Deleted;
// Since Tracks & Episodes are on the same database and handled by dotnet-ef, we can't use the repository to delete them.
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
} }

View File

@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class GenreRepository : LocalRepository<Genre, GenreDE>, IGenreRepository public class GenreRepository : LocalRepository<Genre>, IGenreRepository
{ {
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
protected override Expression<Func<GenreDE, object>> DefaultSort => x => x.Slug; protected override Expression<Func<Genre, object>> DefaultSort => x => x.Slug;
public GenreRepository(DatabaseContext database) : base(database) public GenreRepository(DatabaseContext database) : base(database)
@ -26,6 +26,7 @@ namespace Kyoo.Controllers
return; return;
_disposed = true; _disposed = true;
_database.Dispose(); _database.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -40,11 +41,12 @@ namespace Kyoo.Controllers
{ {
return await _database.Genres return await _database.Genres
.Where(genre => EF.Functions.ILike(genre.Name, $"%{query}%")) .Where(genre => EF.Functions.ILike(genre.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync<Genre>(); .ToListAsync();
} }
public override async Task<GenreDE> Create(GenreDE obj) public override async Task<Genre> Create(Genre obj)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
@ -52,15 +54,12 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
public override async Task Delete(GenreDE obj) public override async Task Delete(Genre obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
if (obj.Links != null)
foreach (GenreLink link in obj.Links)
_database.Entry(link).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
} }

View File

@ -42,6 +42,7 @@ namespace Kyoo.Controllers
_shows.Value.Dispose(); _shows.Value.Dispose();
if (_collections.IsValueCreated) if (_collections.IsValueCreated)
_collections.Value.Dispose(); _collections.Value.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -71,7 +72,7 @@ namespace Kyoo.Controllers
private IQueryable<LibraryItem> ItemsQuery private IQueryable<LibraryItem> ItemsQuery
=> _database.Shows => _database.Shows
.Where(x => !_database.CollectionLinks.Any(y => y.ChildID == x.ID)) .Where(x => !x.Collections.Any())
.Select(LibraryItem.FromShow) .Select(LibraryItem.FromShow)
.Concat(_database.Collections .Concat(_database.Collections
.Select(LibraryItem.FromCollection)); .Select(LibraryItem.FromCollection));
@ -91,10 +92,11 @@ namespace Kyoo.Controllers
return query.CountAsync(); return query.CountAsync();
} }
public async Task<ICollection<LibraryItem>> Search(string query) public override async Task<ICollection<LibraryItem>> Search(string query)
{ {
return await ItemsQuery return await ItemsQuery
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -108,22 +110,19 @@ namespace Kyoo.Controllers
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException();
protected override Task Validate(LibraryItem resource) => throw new InvalidOperationException();
public override Task Delete(int id) => throw new InvalidOperationException(); public override Task Delete(int id) => throw new InvalidOperationException();
public override Task Delete(string slug) => throw new InvalidOperationException(); public override Task Delete(string slug) => throw new InvalidOperationException();
public override Task Delete(LibraryItem obj) => throw new InvalidOperationException(); public override Task Delete(LibraryItem obj) => throw new InvalidOperationException();
private IQueryable<LibraryItem> LibraryRelatedQuery(Expression<Func<LibraryLink, bool>> selector) private IQueryable<LibraryItem> LibraryRelatedQuery(Expression<Func<Library, bool>> selector)
=> _database.LibraryLinks => _database.Libraries
.Where(selector) .Where(selector)
.Select(x => x.Show) .SelectMany(x => x.Shows)
.Where(x => x != null) .Where(x => !x.Collections.Any())
.Where(x => !_database.CollectionLinks.Any(y => y.ChildID == x.ID))
.Select(LibraryItem.FromShow) .Select(LibraryItem.FromShow)
.Concat(_database.LibraryLinks .Concat(_database.Libraries
.Where(selector) .Where(selector)
.Select(x => x.Collection) .SelectMany(x => x.Collections)
.Where(x => x != null)
.Select(LibraryItem.FromCollection)); .Select(LibraryItem.FromCollection));
public async Task<ICollection<LibraryItem>> GetFromLibrary(int id, public async Task<ICollection<LibraryItem>> GetFromLibrary(int id,
@ -131,7 +130,7 @@ namespace Kyoo.Controllers
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<LibraryItem> items = await ApplyFilters(LibraryRelatedQuery(x => x.LibraryID == id), ICollection<LibraryItem> items = await ApplyFilters(LibraryRelatedQuery(x => x.ID == id),
where, where,
sort, sort,
limit); limit);
@ -145,7 +144,7 @@ namespace Kyoo.Controllers
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<LibraryItem> items = await ApplyFilters(LibraryRelatedQuery(x => x.Library.Slug == slug), ICollection<LibraryItem> items = await ApplyFilters(LibraryRelatedQuery(x => x.Slug == slug),
where, where,
sort, sort,
limit); limit);

View File

@ -4,18 +4,16 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
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;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class LibraryRepository : LocalRepository<Library, LibraryDE>, ILibraryRepository public class LibraryRepository : LocalRepository<Library>, ILibraryRepository
{ {
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
protected override Expression<Func<LibraryDE, object>> DefaultSort => x => x.ID; protected override Expression<Func<Library, object>> DefaultSort => x => x.ID;
public LibraryRepository(DatabaseContext database, IProviderRepository providers) public LibraryRepository(DatabaseContext database, IProviderRepository providers)
@ -32,6 +30,7 @@ namespace Kyoo.Controllers
_disposed = true; _disposed = true;
_database.Dispose(); _database.Dispose();
_providers.Dispose(); _providers.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -47,23 +46,30 @@ namespace Kyoo.Controllers
{ {
return await _database.Libraries return await _database.Libraries
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync<Library>(); .ToListAsync();
} }
public override async Task<LibraryDE> Create(LibraryDE obj) public override async Task<Library> Create(Library obj)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
if (obj.ProviderLinks != null) obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToArray();
foreach (ProviderLink entry in obj.ProviderLinks) obj.ProviderLinks.ForEach(x => _database.Entry(x).State = EntityState.Added);
_database.Entry(entry).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists).");
return obj; return obj;
} }
protected override async Task Validate(LibraryDE resource) protected override async Task Validate(Library resource)
{
await base.Validate(resource);
resource.Providers = await resource.Providers
.SelectAsync(x => _providers.CreateIfNotExists(x, true))
.ToListAsync();
}
protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{ {
if (string.IsNullOrEmpty(resource.Slug)) if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("The library's slug must be set and not empty"); throw new ArgumentException("The library's slug must be set and not empty");
@ -72,26 +78,20 @@ namespace Kyoo.Controllers
if (resource.Paths == null || !resource.Paths.Any()) if (resource.Paths == null || !resource.Paths.Any())
throw new ArgumentException("The library should have a least one path."); throw new ArgumentException("The library should have a least one path.");
await base.Validate(resource); if (changed.Providers != null || resetOld)
{
if (resource.ProviderLinks != null) await Validate(changed);
foreach (ProviderLink link in resource.ProviderLinks) await Database.Entry(resource).Collection(x => x.Providers).LoadAsync();
if (ShouldValidate(link)) resource.Providers = changed.Providers;
link.Child = await _providers.CreateIfNotExists(link.Child, true); }
} }
public override async Task Delete(LibraryDE obj) public override async Task Delete(Library obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
if (obj.ProviderLinks != null)
foreach (ProviderLink entry in obj.ProviderLinks)
_database.Entry(entry).State = EntityState.Deleted;
if (obj.Links != null)
foreach (LibraryLink entry in obj.Links)
_database.Entry(entry).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
} }

View File

@ -36,6 +36,7 @@ namespace Kyoo.Controllers
_providers.Dispose(); _providers.Dispose();
if (_shows.IsValueCreated) if (_shows.IsValueCreated)
_shows.Value.Dispose(); _shows.Value.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -49,10 +50,11 @@ namespace Kyoo.Controllers
await _shows.Value.DisposeAsync(); await _shows.Value.DisposeAsync();
} }
public async Task<ICollection<People>> Search(string query) public override async Task<ICollection<People>> Search(string query)
{ {
return await _database.People return await _database.People
.Where(people => EF.Functions.ILike(people.Name, $"%{query}%")) .Where(people => EF.Functions.ILike(people.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -61,10 +63,7 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
if (obj.ExternalIDs != null) obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
foreach (MetadataID entry in obj.ExternalIDs)
_database.Entry(entry).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -72,11 +71,35 @@ namespace Kyoo.Controllers
protected override async Task Validate(People resource) protected override async Task Validate(People resource)
{ {
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id =>
{
id.Provider = await _providers.CreateIfNotExists(id.Provider, true);
id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached;
});
await resource.Roles.ForEachAsync(async role =>
{
role.Show = await _shows.Value.CreateIfNotExists(role.Show, true);
role.ShowID = role.Show.ID;
_database.Entry(role.Show).State = EntityState.Detached;
});
}
if (resource.ExternalIDs != null) protected override async Task EditRelations(People resource, People changed, bool resetOld)
foreach (MetadataID link in resource.ExternalIDs) {
if (ShouldValidate(link)) if (changed.Roles != null || resetOld)
link.Provider = await _providers.CreateIfNotExists(link.Provider, true); {
await Database.Entry(resource).Collection(x => x.Roles).LoadAsync();
resource.Roles = changed.Roles;
}
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
await base.EditRelations(resource, changed, resetOld);
} }
public override async Task Delete(People obj) public override async Task Delete(People obj)
@ -85,12 +108,8 @@ namespace Kyoo.Controllers
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
if (obj.ExternalIDs != null) obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
foreach (MetadataID entry in obj.ExternalIDs) obj.Roles.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
_database.Entry(entry).State = EntityState.Deleted;
if (obj.Roles != null)
foreach (PeopleRole link in obj.Roles)
_database.Entry(link).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
@ -99,7 +118,9 @@ namespace Kyoo.Controllers
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles.Where(x => x.ShowID == showID), ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles
.Where(x => x.ShowID == showID)
.Include(x => x.People),
id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
x => x.People.Name, x => x.People.Name,
where, where,
@ -107,6 +128,8 @@ namespace Kyoo.Controllers
limit); limit);
if (!people.Any() && await _shows.Value.Get(showID) == null) if (!people.Any() && await _shows.Value.Get(showID) == null)
throw new ItemNotFound(); throw new ItemNotFound();
foreach (PeopleRole role in people)
role.ForPeople = true;
return people; return people;
} }
@ -115,7 +138,10 @@ namespace Kyoo.Controllers
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles.Where(x => x.Show.Slug == showSlug), ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles
.Where(x => x.Show.Slug == showSlug)
.Include(x => x.People)
.Include(x => x.Show),
id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
x => x.People.Name, x => x.People.Name,
where, where,
@ -123,18 +149,21 @@ namespace Kyoo.Controllers
limit); limit);
if (!people.Any() && await _shows.Value.Get(showSlug) == null) if (!people.Any() && await _shows.Value.Get(showSlug) == null)
throw new ItemNotFound(); throw new ItemNotFound();
foreach (PeopleRole role in people)
role.ForPeople = true;
return people; return people;
} }
public async Task<ICollection<ShowRole>> GetFromPeople(int peopleID, public async Task<ICollection<PeopleRole>> GetFromPeople(int peopleID,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<ShowRole> roles = await ApplyFilters(_database.PeopleRoles.Where(x => x.PeopleID == peopleID) ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
.Select(ShowRole.FromPeopleRole), .Where(x => x.PeopleID == peopleID)
async id => new ShowRole(await _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id)), .Include(x => x.Show),
x => x.Title, id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
x => x.Show.Title,
where, where,
sort, sort,
limit); limit);
@ -143,15 +172,16 @@ namespace Kyoo.Controllers
return roles; return roles;
} }
public async Task<ICollection<ShowRole>> GetFromPeople(string slug, public async Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<ShowRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<ShowRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<ShowRole> roles = await ApplyFilters(_database.PeopleRoles.Where(x => x.People.Slug == slug) ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
.Select(ShowRole.FromPeopleRole), .Where(x => x.People.Slug == slug)
async id => new ShowRole(await _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id)), .Include(x => x.Show),
x => x.Title, id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
x => x.Show.Title,
where, where,
sort, sort,
limit); limit);

View File

@ -19,10 +19,11 @@ namespace Kyoo.Controllers
_database = database; _database = database;
} }
public async Task<ICollection<ProviderID>> Search(string query) public override async Task<ICollection<ProviderID>> Search(string query)
{ {
return await _database.Providers return await _database.Providers
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -31,7 +32,6 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated provider (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated provider (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -42,8 +42,20 @@ namespace Kyoo.Controllers
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
// TODO handle ExternalID deletion when they refer to this providerID. obj.MetadataLinks.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
public Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default)
{
return ApplyFilters(_database.MetadataIds.Include(y => y.Provider),
x => _database.MetadataIds.FirstOrDefaultAsync(y => y.ID == x),
x => x.ID,
where,
sort,
limit);
}
} }
} }

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,8 +40,10 @@ 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);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -49,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*)");
@ -62,34 +83,51 @@ 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 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}%"))
.OrderBy(DefaultSort)
.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)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
if (obj.ExternalIDs != null) obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
foreach (MetadataID entry in obj.ExternalIDs)
_database.Entry(entry).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated season (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated season (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -100,13 +138,22 @@ namespace Kyoo.Controllers
throw new InvalidOperationException($"Can't store a season not related to any show (showID: {resource.ShowID})."); throw new InvalidOperationException($"Can't store a season not related to any show (showID: {resource.ShowID}).");
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id =>
if (resource.ExternalIDs != null)
{ {
foreach (MetadataID link in resource.ExternalIDs) id.Provider = await _providers.CreateIfNotExists(id.Provider, true);
if (ShouldValidate(link)) id.ProviderID = id.Provider.ID;
link.Provider = await _providers.CreateIfNotExists(link.Provider, true); _database.Entry(id.Provider).State = EntityState.Detached;
});
} }
protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
{
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
await base.EditRelations(resource, changed, resetOld);
} }
public async Task Delete(string showSlug, int seasonNumber) public async Task Delete(string showSlug, int seasonNumber)
@ -121,11 +168,7 @@ namespace Kyoo.Controllers
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
if (obj.ExternalIDs != null)
foreach (MetadataID entry in obj.ExternalIDs)
_database.Entry(entry).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
if (obj.Episodes != null) if (obj.Episodes != null)

View File

@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class ShowRepository : LocalRepository<Show, ShowDE>, IShowRepository public class ShowRepository : LocalRepository<Show>, IShowRepository
{ {
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
@ -19,7 +19,7 @@ namespace Kyoo.Controllers
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
private readonly Lazy<ISeasonRepository> _seasons; private readonly Lazy<ISeasonRepository> _seasons;
private readonly Lazy<IEpisodeRepository> _episodes; private readonly Lazy<IEpisodeRepository> _episodes;
protected override Expression<Func<ShowDE, object>> DefaultSort => x => x.Title; protected override Expression<Func<Show, object>> DefaultSort => x => x.Title;
public ShowRepository(DatabaseContext database, public ShowRepository(DatabaseContext database,
IStudioRepository studios, IStudioRepository studios,
@ -52,6 +52,7 @@ namespace Kyoo.Controllers
_seasons.Value.Dispose(); _seasons.Value.Dispose();
if (_episodes.IsValueCreated) if (_episodes.IsValueCreated)
_episodes.Value.Dispose(); _episodes.Value.Dispose();
GC.SuppressFinalize(this);
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -77,89 +78,110 @@ namespace Kyoo.Controllers
.Where(x => EF.Functions.ILike(x.Title, query) .Where(x => EF.Functions.ILike(x.Title, query)
|| EF.Functions.ILike(x.Slug, query) || EF.Functions.ILike(x.Slug, query)
/*|| x.Aliases.Any(y => EF.Functions.ILike(y, query))*/) // NOT TRANSLATABLE. /*|| x.Aliases.Any(y => EF.Functions.ILike(y, query))*/) // NOT TRANSLATABLE.
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync<Show>(); .ToListAsync();
} }
public override async Task<ShowDE> Create(ShowDE obj) public override async Task<Show> Create(Show obj)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
obj.GenreLinks.ForEach(x => _database.Entry(x).State = EntityState.Added);
if (obj.GenreLinks != null) obj.People.ForEach(x => _database.Entry(x).State = EntityState.Added);
{ obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
foreach (GenreLink entry in obj.GenreLinks)
{
if (!(entry.Child is GenreDE))
entry.Child = new GenreDE(entry.Child);
_database.Entry(entry).State = EntityState.Added;
}
}
if (obj.People != null)
foreach (PeopleRole entry in obj.People)
_database.Entry(entry).State = EntityState.Added;
if (obj.ExternalIDs != null)
foreach (MetadataID entry in obj.ExternalIDs)
_database.Entry(entry).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated show (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated show (slug {obj.Slug} already exists).");
return obj; return obj;
} }
protected override async Task Validate(ShowDE resource) protected override async Task Validate(Show resource)
{ {
await base.Validate(resource); await base.Validate(resource);
if (resource.Studio != null)
if (ShouldValidate(resource.Studio))
resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true); resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true);
resource.Genres = await resource.Genres
.SelectAsync(x => _genres.CreateIfNotExists(x, true))
.ToListAsync();
resource.GenreLinks = resource.Genres?
.Select(x => Link.UCreate(resource, x))
.ToList();
await resource.ExternalIDs.ForEachAsync(async id =>
{
id.Provider = await _providers.CreateIfNotExists(id.Provider, true);
id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached;
});
await resource.People.ForEachAsync(async role =>
{
role.People = await _people.CreateIfNotExists(role.People, true);
role.PeopleID = role.People.ID;
_database.Entry(role.People).State = EntityState.Detached;
});
}
if (resource.GenreLinks != null) protected override async Task EditRelations(Show resource, Show changed, bool resetOld)
foreach (GenreLink link in resource.GenreLinks) {
if (ShouldValidate(link)) await Validate(changed);
link.Child = await _genres.CreateIfNotExists(link.Child, true);
if (resource.People != null) if (changed.Aliases != null || resetOld)
foreach (PeopleRole link in resource.People) resource.Aliases = changed.Aliases;
if (ShouldValidate(link))
link.People = await _people.CreateIfNotExists(link.People, true);
if (resource.ExternalIDs != null) if (changed.Genres != null || resetOld)
foreach (MetadataID link in resource.ExternalIDs) {
if (ShouldValidate(link)) await Database.Entry(resource).Collection(x => x.GenreLinks).LoadAsync();
link.Provider = await _providers.CreateIfNotExists(link.Provider, true); resource.GenreLinks = changed.Genres?.Select(x => Link.UCreate(resource, x)).ToList();
}
if (changed.People != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.People).LoadAsync();
resource.People = changed.People;
}
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
} }
public async Task AddShowLink(int showID, int? libraryID, int? collectionID) public async Task AddShowLink(int showID, int? libraryID, int? collectionID)
{ {
if (collectionID != null) if (collectionID != null)
{ {
await _database.CollectionLinks.AddAsync(new CollectionLink {ParentID = collectionID.Value, ChildID = showID}); await _database.Links<Collection, Show>()
.AddAsync(new Link<Collection, Show>(collectionID.Value, showID));
await _database.SaveIfNoDuplicates(); await _database.SaveIfNoDuplicates();
if (libraryID != null)
{
await _database.Links<Library, Collection>()
.AddAsync(new Link<Library, Collection>(libraryID.Value, collectionID.Value));
await _database.SaveIfNoDuplicates();
}
} }
if (libraryID != null) if (libraryID != null)
{ {
await _database.LibraryLinks.AddAsync(new LibraryLink {LibraryID = libraryID.Value, ShowID = showID}); await _database.Links<Library, Show>()
.AddAsync(new Link<Library, Show>(libraryID.Value, showID));
await _database.SaveIfNoDuplicates(); await _database.SaveIfNoDuplicates();
} }
}
if (libraryID != null && collectionID != null) public Task<string> GetSlug(int showID)
{ {
await _database.LibraryLinks.AddAsync(new LibraryLink {LibraryID = libraryID.Value, CollectionID = collectionID.Value}); return _database.Shows.Where(x => x.ID == showID)
await _database.SaveIfNoDuplicates(); .Select(x => x.Slug)
} .FirstOrDefaultAsync();
} }
public override async Task Delete(ShowDE obj) public override async Task Delete(Show obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
if (obj.GenreLinks != null)
foreach (GenreLink entry in obj.GenreLinks)
_database.Entry(entry).State = EntityState.Deleted;
if (obj.People != null) if (obj.People != null)
foreach (PeopleRole entry in obj.People) foreach (PeopleRole entry in obj.People)
@ -169,14 +191,6 @@ namespace Kyoo.Controllers
foreach (MetadataID entry in obj.ExternalIDs) foreach (MetadataID entry in obj.ExternalIDs)
_database.Entry(entry).State = EntityState.Deleted; _database.Entry(entry).State = EntityState.Deleted;
if (obj.CollectionLinks != null)
foreach (CollectionLink entry in obj.CollectionLinks)
_database.Entry(entry).State = EntityState.Deleted;
if (obj.LibraryLinks != null)
foreach (LibraryLink entry in obj.LibraryLinks)
_database.Entry(entry).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
if (obj.Seasons != null) if (obj.Seasons != null)

View File

@ -19,10 +19,11 @@ namespace Kyoo.Controllers
_database = database; _database = database;
} }
public async Task<ICollection<Studio>> Search(string query) public override async Task<ICollection<Studio>> Search(string query)
{ {
return await _database.Studios return await _database.Studios
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -41,10 +42,6 @@ namespace Kyoo.Controllers
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
// Using Dotnet-EF change discovery service to remove references to this studio on shows.
foreach (Show show in obj.Shows)
show.StudioID = null;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
} }

View File

@ -4,6 +4,7 @@ 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
@ -12,7 +13,7 @@ namespace Kyoo.Controllers
{ {
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
protected override Expression<Func<Track, object>> DefaultSort => x => x.ID; protected override Expression<Func<Track, object>> DefaultSort => x => x.TrackIndex;
public TrackRepository(DatabaseContext database) : base(database) public TrackRepository(DatabaseContext database) : base(database)
@ -36,10 +37,15 @@ namespace Kyoo.Controllers
await _database.DisposeAsync(); await _database.DisposeAsync();
} }
public Task<Track> Get(string slug, StreamType type = StreamType.Unknown) public override Task<Track> Get(string slug)
{
return Get(slug, StreamType.Unknown);
}
public Task<Track> Get(string slug, StreamType type)
{ {
Match match = Regex.Match(slug, Match match = Regex.Match(slug,
@"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?"); @"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success) if (!match.Success)
{ {
@ -56,6 +62,8 @@ namespace Kyoo.Controllers
int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1; int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1;
string language = match.Groups["language"].Value; string language = match.Groups["language"].Value;
bool forced = match.Groups["forced"].Success; bool forced = match.Groups["forced"].Success;
if (match.Groups["type"].Success)
type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
if (type == StreamType.Unknown) if (type == StreamType.Unknown)
{ {
@ -73,7 +81,7 @@ namespace Kyoo.Controllers
&& x.IsForced == forced); && x.IsForced == forced);
} }
public Task<ICollection<Track>> Search(string query) public override Task<ICollection<Track>> Search(string query)
{ {
throw new InvalidOperationException("Tracks do not support the search method."); throw new InvalidOperationException("Tracks do not support the search method.");
} }
@ -82,20 +90,21 @@ 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}).");
} }
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated track (slug {obj.Slug} already exists)."); await _database.SaveOrRetry(obj, (x, i) =>
return obj;
}
protected override Task Validate(Track resource)
{ {
return Task.CompletedTask; if (i > 10)
throw new DuplicatedItemException($"More than 10 same tracks exists {x.Slug}. Aborting...");
x.TrackIndex++;
return x;
});
return obj;
} }
public override async Task Delete(Track obj) public override async Task Delete(Track obj)

View File

@ -110,7 +110,7 @@ namespace Kyoo.Controllers
_tasks.AddRange(CoreTaskHolder.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug)))); _tasks.AddRange(CoreTaskHolder.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))));
IEnumerable<ITask> prerunTasks = _tasks.Select(x => x.task) IEnumerable<ITask> prerunTasks = _tasks.Select(x => x.task)
.Where(x => x.RunOnStartup && x.Priority == Int32.MaxValue); .Where(x => x.RunOnStartup && x.Priority == int.MaxValue);
foreach (ITask task in prerunTasks) foreach (ITask task in prerunTasks)
task.Run(_serviceProvider, _taskToken.Token); task.Run(_serviceProvider, _taskToken.Token);

View File

@ -1,20 +1,28 @@
using Kyoo.Models; using Kyoo.Models;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class ThumbnailsManager : IThumbnailsManager public class ThumbnailsManager : IThumbnailsManager
{ {
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly IFileManager _files;
private readonly string _peoplePath;
private readonly string _providerPath;
public ThumbnailsManager(IConfiguration configuration) public ThumbnailsManager(IConfiguration configuration, IFileManager files)
{ {
_config = configuration; _config = configuration;
_files = files;
_peoplePath = Path.GetFullPath(configuration.GetValue<string>("peoplePath"));
_providerPath = Path.GetFullPath(configuration.GetValue<string>("providerPath"));
Directory.CreateDirectory(_peoplePath);
Directory.CreateDirectory(_providerPath);
} }
private static async Task DownloadImage(string url, string localPath, string what) private static async Task DownloadImage(string url, string localPath, string what)
@ -26,83 +34,133 @@ namespace Kyoo.Controllers
} }
catch (WebException exception) catch (WebException exception)
{ {
await Console.Error.WriteLineAsync($"{what} could not be downloaded.\n\tError: {exception.Message}."); await Console.Error.WriteLineAsync($"{what} could not be downloaded. Error: {exception.Message}.");
} }
} }
public async Task<Show> Validate(Show show, bool alwaysDownload) public async Task Validate(Show show, bool alwaysDownload)
{ {
if (show?.Path == null)
return default;
if (show.Poster != null) if (show.Poster != null)
{ {
string posterPath = Path.Combine(show.Path, "poster.jpg"); string posterPath = await GetShowPoster(show);
if (alwaysDownload || !File.Exists(posterPath)) if (alwaysDownload || !File.Exists(posterPath))
await DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}"); await DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}");
} }
if (show.Logo != null) if (show.Logo != null)
{ {
string logoPath = Path.Combine(show.Path, "logo.png"); string logoPath = await GetShowLogo(show);
if (alwaysDownload || !File.Exists(logoPath)) if (alwaysDownload || !File.Exists(logoPath))
await DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}"); await DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}");
} }
if (show.Backdrop != null) if (show.Backdrop != null)
{ {
string backdropPath = Path.Combine(show.Path, "backdrop.jpg"); string backdropPath = await GetShowBackdrop(show);
if (alwaysDownload || !File.Exists(backdropPath)) if (alwaysDownload || !File.Exists(backdropPath))
await DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}"); await DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}");
} }
return show; foreach (PeopleRole role in show.People)
await Validate(role.People, alwaysDownload);
} }
public async Task<IEnumerable<PeopleRole>> Validate(IEnumerable<PeopleRole> people, bool alwaysDownload) public async Task Validate([NotNull] People people, bool alwaysDownload)
{ {
if (people == null) if (people == null)
return null; throw new ArgumentNullException(nameof(people));
string root = _config.GetValue<string>("peoplePath"); string root = _config.GetValue<string>("peoplePath");
string localPath = Path.Combine(root, people.Slug + ".jpg");
Directory.CreateDirectory(root); Directory.CreateDirectory(root);
foreach (PeopleRole peop in people)
{
string localPath = Path.Combine(root, peop.People.Slug + ".jpg");
if (peop.People.Poster == null)
continue;
if (alwaysDownload || !File.Exists(localPath)) if (alwaysDownload || !File.Exists(localPath))
await DownloadImage(peop.People.Poster, localPath, $"The profile picture of {peop.People.Name}"); await DownloadImage(people.Poster, localPath, $"The profile picture of {people.Name}");
} }
return people; public async Task Validate(Season season, bool alwaysDownload)
}
public async Task<Season> Validate(Season season, bool alwaysDownload)
{ {
if (season?.Show?.Path == null) if (season?.Show?.Path == null || season.Poster == null)
return default; return;
if (season.Poster != null) string localPath = await GetSeasonPoster(season);
{
string localPath = Path.Combine(season.Show.Path, $"season-{season.SeasonNumber}.jpg");
if (alwaysDownload || !File.Exists(localPath)) if (alwaysDownload || !File.Exists(localPath))
await DownloadImage(season.Poster, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}"); await DownloadImage(season.Poster, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}");
} }
return season;
}
public async Task<Episode> Validate(Episode episode, bool alwaysDownload) public async Task Validate(Episode episode, bool alwaysDownload)
{ {
if (episode?.Path == null) if (episode?.Path == null || episode.Thumb == null)
return default; return;
if (episode.Poster != null) string localPath = await GetEpisodeThumb(episode);
{
string localPath = Path.ChangeExtension(episode.Path, "jpg");
if (alwaysDownload || !File.Exists(localPath)) if (alwaysDownload || !File.Exists(localPath))
await DownloadImage(episode.Poster, localPath, $"The thumbnail of {episode.Show.Title}"); await DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}");
} }
return episode;
public async Task Validate(ProviderID provider, bool alwaysDownload)
{
if (provider.Logo == null)
return;
string root = _config.GetValue<string>("providerPath");
string localPath = Path.Combine(root, provider.Slug + ".jpg");
Directory.CreateDirectory(root);
if (alwaysDownload || !File.Exists(localPath))
await DownloadImage(provider.Logo, localPath, $"The logo of {provider.Slug}");
}
public Task<string> GetShowBackdrop(Show show)
{
if (show?.Path == null)
throw new ArgumentNullException(nameof(show));
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "backdrop.jpg"));
}
public Task<string> GetShowLogo(Show show)
{
if (show?.Path == null)
throw new ArgumentNullException(nameof(show));
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "logo.png"));
}
public Task<string> GetShowPoster(Show show)
{
if (show?.Path == null)
throw new ArgumentNullException(nameof(show));
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "poster.jpg"));
}
public Task<string> GetSeasonPoster(Season season)
{
if (season == null)
throw new ArgumentNullException(nameof(season));
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(season), $"season-{season.SeasonNumber}.jpg"));
}
public Task<string> GetEpisodeThumb(Episode episode)
{
string dir = Path.Combine(_files.GetExtraDirectory(episode), "Thumbnails");
Directory.CreateDirectory(dir);
return Task.FromResult(Path.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg"));
}
public Task<string> GetPeoplePoster(People people)
{
if (people == null)
throw new ArgumentNullException(nameof(people));
string thumbPath = Path.GetFullPath(Path.Combine(_peoplePath, $"{people.Slug}.jpg"));
if (!thumbPath.StartsWith(_peoplePath))
return Task.FromResult<string>(null);
return Task.FromResult(thumbPath);
}
public Task<string> GetProviderLogo(ProviderID provider)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));
string thumbPath = Path.GetFullPath(Path.Combine(_providerPath, $"{provider.Slug}.jpg"));
if (!thumbPath.StartsWith(_providerPath))
return Task.FromResult<string>(null);
return Task.FromResult(thumbPath.StartsWith(_providerPath) ? thumbPath : null);
} }
} }
} }

View File

@ -0,0 +1,135 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Kyoo.Models;
using Microsoft.Extensions.Configuration;
using Stream = Kyoo.Models.Watch.Stream;
// We use threads so tasks are not always awaited.
#pragma warning disable 4014
namespace Kyoo.Controllers
{
public class BadTranscoderException : Exception {}
public class Transcoder : ITranscoder
{
private static class TranscoderAPI
{
private const string TranscoderPath = "libtranscoder.so";
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int init();
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transmux(string path, string outpath, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transcode(string path, string outpath, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr extract_infos(string path,
string outpath,
out int length,
out int trackCount,
bool reextracct);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern void free(IntPtr ptr);
public static Track[] ExtractInfos(string path, string outPath, bool reextract)
{
int size = Marshal.SizeOf<Stream>();
IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount, reextract);
IntPtr streamsPtr = ptr;
Track[] tracks;
if (trackCount > 0 && ptr != IntPtr.Zero)
{
tracks = new Track[trackCount];
int j = 0;
for (int i = 0; i < arrayLength; i++)
{
Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr);
if (stream!.Type != StreamType.Unknown)
{
tracks[j] = new Track(stream);
j++;
}
streamsPtr += size;
}
}
else
tracks = Array.Empty<Track>();
if (ptr != IntPtr.Zero)
free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers.
return tracks;
}
}
private readonly IFileManager _files;
private readonly string _transmuxPath;
private readonly string _transcodePath;
public Transcoder(IConfiguration config, IFileManager files)
{
_files = files;
_transmuxPath = Path.GetFullPath(config.GetValue<string>("transmuxTempPath"));
_transcodePath = Path.GetFullPath(config.GetValue<string>("transcodeTempPath"));
if (TranscoderAPI.init() != Marshal.SizeOf<Stream>())
throw new BadTranscoderException();
}
public Task<Track[]> ExtractInfos(Episode episode, bool reextract)
{
string dir = _files.GetExtraDirectory(episode);
if (dir == null)
throw new ArgumentException("Invalid path.");
return Task.Factory.StartNew(
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract),
TaskCreationOptions.LongRunning);
}
public async Task<string> Transmux(Episode episode)
{
if (!File.Exists(episode.Path))
throw new ArgumentException("Path does not exists. Can't transcode.");
string folder = Path.Combine(_transmuxPath, episode.Slug);
string manifest = Path.Combine(folder, episode.Slug + ".m3u8");
float playableDuration = 0;
bool transmuxFailed = false;
try
{
Directory.CreateDirectory(folder);
if (File.Exists(manifest))
return manifest;
}
catch (UnauthorizedAccessException)
{
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config.");
return null;
}
Task.Factory.StartNew(() =>
{
string cleanManifest = manifest.Replace('\\', '/');
transmuxFailed = TranscoderAPI.transmux(episode.Path, cleanManifest, out playableDuration) != 0;
}, TaskCreationOptions.LongRunning);
while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed)
await Task.Delay(10);
return transmuxFailed ? null : manifest;
}
public Task<string> Transcode(Episode episode)
{
return Task.FromResult<string>(null); // Not implemented yet.
}
}
}

View File

@ -1,70 +0,0 @@
using System;
using Kyoo.Models;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Kyoo.Controllers.TranscoderLink;
#pragma warning disable 4014
namespace Kyoo.Controllers
{
public class BadTranscoderException : Exception {}
public class Transcoder : ITranscoder
{
private readonly string _transmuxPath;
private readonly string _transcodePath;
public Transcoder(IConfiguration config)
{
_transmuxPath = Path.GetFullPath(config.GetValue<string>("transmuxTempPath"));
_transcodePath = Path.GetFullPath(config.GetValue<string>("transcodeTempPath"));
if (TranscoderAPI.init() != Marshal.SizeOf<Models.Watch.Stream>())
throw new BadTranscoderException();
}
public Task<Track[]> ExtractInfos(string path)
{
string dir = Path.GetDirectoryName(path);
if (dir == null)
throw new ArgumentException("Invalid path.");
return Task.Factory.StartNew(() => TranscoderAPI.ExtractInfos(path, dir), TaskCreationOptions.LongRunning);
}
public async Task<string> Transmux(Episode episode)
{
string folder = Path.Combine(_transmuxPath, episode.Slug);
string manifest = Path.Combine(folder, episode.Slug + ".m3u8");
float playableDuration = 0;
bool transmuxFailed = false;
try
{
Directory.CreateDirectory(folder);
if (File.Exists(manifest))
return manifest;
}
catch (UnauthorizedAccessException)
{
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config.");
return null;
}
Task.Factory.StartNew(() =>
{
transmuxFailed = TranscoderAPI.transmux(episode.Path, manifest.Replace('\\', '/'), out playableDuration) != 0;
}, TaskCreationOptions.LongRunning);
while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed)
await Task.Delay(10);
return transmuxFailed ? null : manifest;
}
public Task<string> Transcode(Episode episode)
{
return Task.FromResult<string>(null); // Not implemented yet.
}
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Kyoo.Models;
using Kyoo.Models.Watch;
// ReSharper disable InconsistentNaming
namespace Kyoo.Controllers.TranscoderLink
{
public static class TranscoderAPI
{
private const string TranscoderPath = "libtranscoder.so";
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int init();
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transmux(string path, string out_path, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transcode(string path, string out_path, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr extract_infos(string path, string outpath, out int length, out int track_count);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern void free(IntPtr stream_ptr);
public static Track[] ExtractInfos(string path, string outPath)
{
int size = Marshal.SizeOf<Stream>();
IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount);
IntPtr streamsPtr = ptr;
Track[] tracks;
if (trackCount > 0 && ptr != IntPtr.Zero)
{
tracks = new Track[trackCount];
int j = 0;
for (int i = 0; i < arrayLength; i++)
{
Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr);
if (stream!.Type != StreamType.Unknown)
{
tracks[j] = new Track(stream);
j++;
}
streamsPtr += size;
}
}
else
tracks = Array.Empty<Track>();
if (ptr != IntPtr.Zero)
free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers.
return tracks;
}
}
}

View File

@ -21,32 +21,32 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IdentityServer4" Version="3.1.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="3.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework.Storage" Version="3.1.2" />
<PackageReference Include="IdentityServer4.Storage" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.6.7" />
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" /> <ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
<PackageReference Include="IdentityServer4" Version="4.1.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.1" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.1" />
<PackageReference Include="IdentityServer4.EntityFramework.Storage" Version="4.1.1" />
<PackageReference Include="IdentityServer4.Storage" Version="4.1.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.2" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.9" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.12" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -90,7 +90,7 @@
</Target> </Target>
<Target Name="Symlink views to output - Linux" AfterTargets="Build" Condition="$(Configuration) == 'Debug' And $(OS) == 'Unix'"> <Target Name="Symlink views to output - Linux" AfterTargets="Build" Condition="$(Configuration) == 'Debug' And $(OS) == 'Unix'">
<Exec WorkingDirectory="$(OutputPath)" Command="ln -fs ../../../Views"/> <Exec WorkingDirectory="$(OutputPath)" Command="ln -fs ../../../Views" />
</Target> </Target>
<Target Name="Compile the transcoder" BeforeTargets="BeforeBuild"> <Target Name="Compile the transcoder" BeforeTargets="BeforeBuild">

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,15 +12,19 @@ namespace Kyoo
{ {
public class DatabaseContext : DbContext public class DatabaseContext : DbContext
{ {
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { } public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.LazyLoadingEnabled = false;
}
public DbSet<LibraryDE> Libraries { get; set; } public DbSet<Library> Libraries { get; set; }
public DbSet<CollectionDE> Collections { get; set; } public DbSet<Collection> Collections { get; set; }
public DbSet<ShowDE> Shows { get; set; } public DbSet<Show> Shows { get; set; }
public DbSet<Season> Seasons { get; set; } public DbSet<Season> Seasons { get; set; }
public DbSet<Episode> Episodes { get; set; } public DbSet<Episode> Episodes { get; set; }
public DbSet<Track> Tracks { get; set; } public DbSet<Track> Tracks { get; set; }
public DbSet<GenreDE> Genres { get; set; } public DbSet<Genre> Genres { get; set; }
public DbSet<People> People { get; set; } public DbSet<People> People { get; set; }
public DbSet<Studio> Studios { get; set; } public DbSet<Studio> Studios { get; set; }
public DbSet<ProviderID> Providers { get; set; } public DbSet<ProviderID> Providers { get; set; }
@ -28,23 +32,23 @@ namespace Kyoo
public DbSet<PeopleRole> PeopleRoles { get; set; } public DbSet<PeopleRole> PeopleRoles { get; set; }
public DbSet<Link<T1, T2>> Links<T1, T2>()
where T1 : class, IResource
where T2 : class, IResource
{
return Set<Link<T1, T2>>();
}
public DbSet<LibraryLink> LibraryLinks { get; set; }
public DbSet<CollectionLink> CollectionLinks { get; set; }
public DbSet<GenreLink> GenreLinks { get; set; }
public DbSet<ProviderLink> ProviderLinks { get; set; }
public DatabaseContext() public DatabaseContext()
{ {
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
}
private readonly ValueComparer<IEnumerable<string>> _stringArrayComparer = ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
new ValueComparer<IEnumerable<string>>( ChangeTracker.LazyLoadingEnabled = false;
(l1, l2) => l1.SequenceEqual(l2), }
arr => arr.Aggregate(0, (i, s) => s.GetHashCode()));
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -54,20 +58,13 @@ namespace Kyoo
modelBuilder.HasPostgresEnum<ItemType>(); modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>(); modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Ignore<Library>(); modelBuilder.Entity<Library>()
modelBuilder.Ignore<Collection>();
modelBuilder.Ignore<Show>();
modelBuilder.Ignore<Genre>();
modelBuilder.Entity<LibraryDE>()
.Property(x => x.Paths) .Property(x => x.Paths)
.HasColumnType("text[]") .HasColumnType("text[]");
.Metadata.SetValueComparer(_stringArrayComparer);
modelBuilder.Entity<ShowDE>() modelBuilder.Entity<Show>()
.Property(x => x.Aliases) .Property(x => x.Aliases)
.HasColumnType("text[]") .HasColumnType("text[]");
.Metadata.SetValueComparer(_stringArrayComparer);
modelBuilder.Entity<Track>() modelBuilder.Entity<Track>()
.Property(t => t.IsDefault) .Property(t => t.IsDefault)
@ -77,91 +74,69 @@ namespace Kyoo
.Property(t => t.IsForced) .Property(t => t.IsForced)
.ValueGeneratedNever(); .ValueGeneratedNever();
modelBuilder.Entity<ProviderID>()
.HasMany(x => x.Libraries)
.WithMany(x => x.Providers)
.UsingEntity<Link<Library, ProviderID>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.ProviderLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, ProviderID>.PrimaryKey));
modelBuilder.Entity<GenreLink>() modelBuilder.Entity<Collection>()
.HasKey(x => new {ShowID = x.ParentID, GenreID = x.ChildID}); .HasMany(x => x.Libraries)
.WithMany(x => x.Collections)
.UsingEntity<Link<Library, Collection>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.CollectionLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, Collection>.PrimaryKey));
modelBuilder.Entity<CollectionLink>() modelBuilder.Entity<Show>()
.HasKey(x => new {CollectionID = x.ParentID, ShowID = x.ChildID}); .HasMany(x => x.Libraries)
.WithMany(x => x.Shows)
.UsingEntity<Link<Library, Show>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.ShowLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, Show>.PrimaryKey));
modelBuilder.Entity<ProviderLink>() modelBuilder.Entity<Show>()
.HasKey(x => new {LibraryID = x.ParentID, ProviderID = x.ChildID}); .HasMany(x => x.Collections)
.WithMany(x => x.Shows)
.UsingEntity<Link<Collection, Show>>(
modelBuilder.Entity<LibraryDE>() y => y
.Ignore(x => x.Shows) .HasOne(x => x.First)
.Ignore(x => x.Collections) .WithMany(x => x.ShowLinks),
.Ignore(x => x.Providers); y => y
.HasOne(x => x.Second)
modelBuilder.Entity<CollectionDE>() .WithMany(x => x.CollectionLinks),
.Ignore(x => x.Shows) y => y.HasKey(Link<Collection, Show>.PrimaryKey));
.Ignore(x => x.Libraries);
modelBuilder.Entity<ShowDE>()
.Ignore(x => x.Genres)
.Ignore(x => x.Libraries)
.Ignore(x => x.Collections);
modelBuilder.Entity<PeopleRole>()
.Ignore(x => x.Slug)
.Ignore(x => x.Name)
.Ignore(x => x.Poster)
.Ignore(x => x.ExternalIDs);
modelBuilder.Entity<GenreDE>()
.Ignore(x => x.Shows);
modelBuilder.Entity<LibraryLink>()
.HasOne(x => x.Library as LibraryDE)
.WithMany(x => x.Links)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<LibraryLink>()
.HasOne(x => x.Show as ShowDE)
.WithMany(x => x.LibraryLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<LibraryLink>()
.HasOne(x => x.Collection as CollectionDE)
.WithMany(x => x.LibraryLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<CollectionLink>()
.HasOne(x => x.Parent as CollectionDE)
.WithMany(x => x.Links)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<CollectionLink>()
.HasOne(x => x.Child as ShowDE)
.WithMany(x => x.CollectionLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GenreLink>()
.HasOne(x => x.Child as GenreDE)
.WithMany(x => x.Links)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GenreLink>()
.HasOne(x => x.Parent as ShowDE)
.WithMany(x => x.GenreLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ProviderLink>()
.HasOne(x => x.Parent as LibraryDE)
.WithMany(x => x.ProviderLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Season>()
.HasOne(x => x.Show as ShowDE)
.WithMany(x => x.Seasons);
modelBuilder.Entity<Episode>()
.HasOne(x => x.Show as ShowDE)
.WithMany(x => x.Episodes);
modelBuilder.Entity<PeopleRole>()
.HasOne(x => x.Show as ShowDE)
.WithMany(x => x.People);
modelBuilder.Entity<Genre>()
.HasMany(x => x.Shows)
.WithMany(x => x.Genres)
.UsingEntity<Link<Show, Genre>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.GenreLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<Show, Genre>.PrimaryKey));
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Show as ShowDE) .HasOne(x => x.Show)
.WithMany(x => x.ExternalIDs) .WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID>()
@ -176,28 +151,32 @@ namespace Kyoo
.HasOne(x => x.People) .HasOne(x => x.People)
.WithMany(x => x.ExternalIDs) .WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Provider)
.WithMany(x => x.MetadataLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<CollectionDE>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<GenreDE>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<LibraryDE>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<ProviderID>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<ProviderID>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<ShowDE>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<CollectionDE>() modelBuilder.Entity<Collection>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<GenreDE>() modelBuilder.Entity<Genre>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<LibraryDE>() modelBuilder.Entity<Library>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<People>() modelBuilder.Entity<People>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<ShowDE>() modelBuilder.Entity<Show>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Studio>() modelBuilder.Entity<Studio>()
@ -212,14 +191,21 @@ namespace Kyoo
modelBuilder.Entity<Episode>() modelBuilder.Entity<Episode>()
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
.IsUnique(); .IsUnique();
modelBuilder.Entity<LibraryLink>() modelBuilder.Entity<Track>()
.HasIndex(x => new {x.LibraryID, x.ShowID}) .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique();
modelBuilder.Entity<LibraryLink>()
.HasIndex(x => new {x.LibraryID, x.CollectionID})
.IsUnique(); .IsUnique();
} }
public T GetTemporaryObject<T>(T model)
where T : class, IResource
{
T tmp = Set<T>().Local.FirstOrDefault(x => x.ID == model.ID);
if (tmp != null)
return tmp;
Entry(model).State = EntityState.Unchanged;
return model;
}
public override int SaveChanges() public override int SaveChanges()
{ {
try try
@ -266,7 +252,7 @@ namespace Kyoo
} }
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = new CancellationToken()) CancellationToken cancellationToken = new())
{ {
try try
{ {
@ -281,7 +267,7 @@ namespace Kyoo
} }
} }
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{ {
try try
{ {
@ -297,7 +283,7 @@ namespace Kyoo
} }
public async Task<int> SaveChangesAsync(string duplicateMessage, public async Task<int> SaveChangesAsync(string duplicateMessage,
CancellationToken cancellationToken = new CancellationToken()) CancellationToken cancellationToken = new())
{ {
try try
{ {
@ -312,7 +298,7 @@ namespace Kyoo
} }
} }
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = new CancellationToken()) public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = new())
{ {
try try
{ {
@ -324,13 +310,39 @@ namespace Kyoo
} }
} }
public static bool IsDuplicateException(DbUpdateException ex) public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new())
{ {
return ex.InnerException is PostgresException inner return SaveOrRetry(obj, onFail, 0, cancellationToken);
&& inner.SqlState == PostgresErrorCodes.UniqueViolation;
} }
public void DiscardChanges() public async Task<T> SaveOrRetry<T>(T obj,
Func<T, int, T> onFail,
int recurse,
CancellationToken cancellationToken = new())
{
try
{
await base.SaveChangesAsync(true, cancellationToken);
return obj;
}
catch (DbUpdateException ex) when (IsDuplicateException(ex))
{
recurse++;
return await SaveOrRetry(onFail(obj, recurse), onFail, recurse, cancellationToken);
}
catch (DbUpdateException)
{
DiscardChanges();
throw;
}
}
private static bool IsDuplicateException(Exception ex)
{
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};
}
private void DiscardChanges()
{ {
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged
&& x.State != EntityState.Detached)) && x.State != EntityState.Detached))

View File

@ -10,16 +10,16 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
{ {
[DbContext(typeof(ConfigurationDbContext))] [DbContext(typeof(ConfigurationDbContext))]
[Migration("20200526235342_Initial")] [Migration("20210306161631_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{ {
@ -28,16 +28,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AllowedAccessTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("Created") b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<string>("DisplayName") b.Property<string>("DisplayName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -47,12 +51,15 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("NonEditable") b.Property<bool>("NonEditable")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated") b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
@ -76,14 +83,14 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId"); b.HasIndex("ApiResourceId");
b.ToTable("ApiClaims"); b.ToTable("ApiResourceClaims");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
@ -98,22 +105,22 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId"); b.HasIndex("ApiResourceId");
b.ToTable("ApiProperties"); b.ToTable("ApiResourceProperties");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -123,21 +130,80 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<int>("ApiResourceId") b.Property<int>("ApiResourceId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Scope")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceSecrets");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName") b.Property<string>("DisplayName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Emphasize") b.Property<bool>("Emphasize")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Required") b.Property<bool>("Required")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -147,8 +213,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.HasIndex("Name") b.HasIndex("Name")
.IsUnique(); .IsUnique();
@ -162,56 +226,46 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiScopeId") b.Property<int>("ScopeId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiScopeId"); b.HasIndex("ScopeId");
b.ToTable("ApiScopeClaims"); b.ToTable("ApiScopeClaims");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId") b.Property<string>("Key")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasColumnType("character varying(1000)")
.HasMaxLength(1000);
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<int>("ScopeId")
.HasColumnType("integer");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(4000)") .HasMaxLength(2000)
.HasMaxLength(4000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId"); b.HasIndex("ScopeId");
b.ToTable("ApiSecrets"); b.ToTable("ApiScopeProperties");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
@ -242,6 +296,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<bool>("AllowRememberConsent") b.Property<bool>("AllowRememberConsent")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("AllowedIdentityTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<bool>("AlwaysIncludeUserClaimsInIdToken") b.Property<bool>("AlwaysIncludeUserClaimsInIdToken")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -255,25 +313,25 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("BackChannelLogoutUri") b.Property<string>("BackChannelLogoutUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<string>("ClientClaimsPrefix") b.Property<string>("ClientClaimsPrefix")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientId") b.Property<string>("ClientId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientName") b.Property<string>("ClientName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientUri") b.Property<string>("ClientUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<int?>("ConsentLifetime") b.Property<int?>("ConsentLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -282,8 +340,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<int>("DeviceCodeLifetime") b.Property<int>("DeviceCodeLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -298,8 +356,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("FrontChannelLogoutUri") b.Property<string>("FrontChannelLogoutUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<int>("IdentityTokenLifetime") b.Property<int>("IdentityTokenLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -311,20 +369,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("LogoUri") b.Property<string>("LogoUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<bool>("NonEditable") b.Property<bool>("NonEditable")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("PairWiseSubjectSalt") b.Property<string>("PairWiseSubjectSalt")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ProtocolType") b.Property<string>("ProtocolType")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<int>("RefreshTokenExpiration") b.Property<int>("RefreshTokenExpiration")
.HasColumnType("integer"); .HasColumnType("integer");
@ -341,6 +399,9 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<bool>("RequirePkce") b.Property<bool>("RequirePkce")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("RequireRequestObject")
.HasColumnType("boolean");
b.Property<int>("SlidingRefreshTokenLifetime") b.Property<int>("SlidingRefreshTokenLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -351,8 +412,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("UserCodeType") b.Property<string>("UserCodeType")
.HasColumnType("character varying(100)") .HasMaxLength(100)
.HasMaxLength(100); .HasColumnType("character varying(100)");
b.Property<int?>("UserSsoLifetime") b.Property<int?>("UserSsoLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -377,13 +438,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.HasKey("Id"); b.HasKey("Id");
@ -404,8 +465,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Origin") b.Property<string>("Origin")
.IsRequired() .IsRequired()
.HasColumnType("character varying(150)") .HasMaxLength(150)
.HasMaxLength(150); .HasColumnType("character varying(150)");
b.HasKey("Id"); b.HasKey("Id");
@ -426,8 +487,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("GrantType") b.Property<string>("GrantType")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.HasKey("Id"); b.HasKey("Id");
@ -448,8 +509,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Provider") b.Property<string>("Provider")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
@ -470,8 +531,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("PostLogoutRedirectUri") b.Property<string>("PostLogoutRedirectUri")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
@ -492,13 +553,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
@ -519,8 +580,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("RedirectUri") b.Property<string>("RedirectUri")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
@ -541,8 +602,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Scope") b.Property<string>("Scope")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
@ -565,21 +626,21 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<DateTime?>("Expiration") b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(4000)") .HasMaxLength(4000)
.HasMaxLength(4000); .HasColumnType("character varying(4000)");
b.HasKey("Id"); b.HasKey("Id");
@ -588,28 +649,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.ToTable("ClientSecrets"); b.ToTable("ClientSecrets");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("character varying(200)")
.HasMaxLength(200);
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -621,12 +660,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<string>("DisplayName") b.Property<string>("DisplayName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Emphasize") b.Property<bool>("Emphasize")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -636,8 +675,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("NonEditable") b.Property<bool>("NonEditable")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -659,6 +698,28 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.ToTable("IdentityResources"); b.ToTable("IdentityResources");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityResourceClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -671,19 +732,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IdentityResourceId"); b.HasIndex("IdentityResourceId");
b.ToTable("IdentityProperties"); b.ToTable("IdentityResourceProperties");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b =>
@ -693,6 +754,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
@ -702,33 +765,52 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{ {
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Scopes") .WithMany("Scopes")
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "ApiScope")
.WithMany("UserClaims")
.HasForeignKey("ApiScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b =>
{ {
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Secrets") .WithMany("Secrets")
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("UserClaims")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("Properties")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b =>
@ -738,6 +820,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b =>
@ -747,6 +831,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b =>
@ -756,6 +842,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b =>
@ -765,6 +853,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
@ -774,6 +864,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b =>
@ -783,6 +875,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b =>
@ -792,6 +886,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b =>
@ -801,6 +897,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b =>
@ -810,15 +908,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{ {
b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource")
.WithMany("UserClaims") .WithMany("UserClaims")
.HasForeignKey("IdentityResourceId") .HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("IdentityResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
@ -828,6 +930,54 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("IdentityResourceId") .HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("IdentityResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{
b.Navigation("Properties");
b.Navigation("Scopes");
b.Navigation("Secrets");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
{
b.Navigation("AllowedCorsOrigins");
b.Navigation("AllowedGrantTypes");
b.Navigation("AllowedScopes");
b.Navigation("Claims");
b.Navigation("ClientSecrets");
b.Navigation("IdentityProviderRestrictions");
b.Navigation("PostLogoutRedirectUris");
b.Navigation("Properties");
b.Navigation("RedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -12,69 +12,92 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ApiResources", name: "ApiResources",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(maxLength: 200, nullable: false), Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(maxLength: 200, nullable: true), DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(maxLength: 1000, nullable: true), Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Created = table.Column<DateTime>(nullable: false), AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
Updated = table.Column<DateTime>(nullable: true), ShowInDiscoveryDocument = table.Column<bool>(type: "boolean", nullable: false),
LastAccessed = table.Column<DateTime>(nullable: true), Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
NonEditable = table.Column<bool>(nullable: false) Updated = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
LastAccessed = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
NonEditable = table.Column<bool>(type: "boolean", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ApiResources", x => x.Id); table.PrimaryKey("PK_ApiResources", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "ApiScopes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Required = table.Column<bool>(type: "boolean", nullable: false),
Emphasize = table.Column<bool>(type: "boolean", nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopes", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Clients", name: "Clients",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
ClientId = table.Column<string>(maxLength: 200, nullable: false), ClientId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ProtocolType = table.Column<string>(maxLength: 200, nullable: false), ProtocolType = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
RequireClientSecret = table.Column<bool>(nullable: false), RequireClientSecret = table.Column<bool>(type: "boolean", nullable: false),
ClientName = table.Column<string>(maxLength: 200, nullable: true), ClientName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(maxLength: 1000, nullable: true), Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
ClientUri = table.Column<string>(maxLength: 2000, nullable: true), ClientUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
LogoUri = table.Column<string>(maxLength: 2000, nullable: true), LogoUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
RequireConsent = table.Column<bool>(nullable: false), RequireConsent = table.Column<bool>(type: "boolean", nullable: false),
AllowRememberConsent = table.Column<bool>(nullable: false), AllowRememberConsent = table.Column<bool>(type: "boolean", nullable: false),
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(nullable: false), AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "boolean", nullable: false),
RequirePkce = table.Column<bool>(nullable: false), RequirePkce = table.Column<bool>(type: "boolean", nullable: false),
AllowPlainTextPkce = table.Column<bool>(nullable: false), AllowPlainTextPkce = table.Column<bool>(type: "boolean", nullable: false),
AllowAccessTokensViaBrowser = table.Column<bool>(nullable: false), RequireRequestObject = table.Column<bool>(type: "boolean", nullable: false),
FrontChannelLogoutUri = table.Column<string>(maxLength: 2000, nullable: true), AllowAccessTokensViaBrowser = table.Column<bool>(type: "boolean", nullable: false),
FrontChannelLogoutSessionRequired = table.Column<bool>(nullable: false), FrontChannelLogoutUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
BackChannelLogoutUri = table.Column<string>(maxLength: 2000, nullable: true), FrontChannelLogoutSessionRequired = table.Column<bool>(type: "boolean", nullable: false),
BackChannelLogoutSessionRequired = table.Column<bool>(nullable: false), BackChannelLogoutUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
AllowOfflineAccess = table.Column<bool>(nullable: false), BackChannelLogoutSessionRequired = table.Column<bool>(type: "boolean", nullable: false),
IdentityTokenLifetime = table.Column<int>(nullable: false), AllowOfflineAccess = table.Column<bool>(type: "boolean", nullable: false),
AccessTokenLifetime = table.Column<int>(nullable: false), IdentityTokenLifetime = table.Column<int>(type: "integer", nullable: false),
AuthorizationCodeLifetime = table.Column<int>(nullable: false), AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ConsentLifetime = table.Column<int>(nullable: true), AccessTokenLifetime = table.Column<int>(type: "integer", nullable: false),
AbsoluteRefreshTokenLifetime = table.Column<int>(nullable: false), AuthorizationCodeLifetime = table.Column<int>(type: "integer", nullable: false),
SlidingRefreshTokenLifetime = table.Column<int>(nullable: false), ConsentLifetime = table.Column<int>(type: "integer", nullable: true),
RefreshTokenUsage = table.Column<int>(nullable: false), AbsoluteRefreshTokenLifetime = table.Column<int>(type: "integer", nullable: false),
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(nullable: false), SlidingRefreshTokenLifetime = table.Column<int>(type: "integer", nullable: false),
RefreshTokenExpiration = table.Column<int>(nullable: false), RefreshTokenUsage = table.Column<int>(type: "integer", nullable: false),
AccessTokenType = table.Column<int>(nullable: false), UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "boolean", nullable: false),
EnableLocalLogin = table.Column<bool>(nullable: false), RefreshTokenExpiration = table.Column<int>(type: "integer", nullable: false),
IncludeJwtId = table.Column<bool>(nullable: false), AccessTokenType = table.Column<int>(type: "integer", nullable: false),
AlwaysSendClientClaims = table.Column<bool>(nullable: false), EnableLocalLogin = table.Column<bool>(type: "boolean", nullable: false),
ClientClaimsPrefix = table.Column<string>(maxLength: 200, nullable: true), IncludeJwtId = table.Column<bool>(type: "boolean", nullable: false),
PairWiseSubjectSalt = table.Column<string>(maxLength: 200, nullable: true), AlwaysSendClientClaims = table.Column<bool>(type: "boolean", nullable: false),
Created = table.Column<DateTime>(nullable: false), ClientClaimsPrefix = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Updated = table.Column<DateTime>(nullable: true), PairWiseSubjectSalt = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
LastAccessed = table.Column<DateTime>(nullable: true), Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
UserSsoLifetime = table.Column<int>(nullable: true), Updated = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
UserCodeType = table.Column<string>(maxLength: 100, nullable: true), LastAccessed = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
DeviceCodeLifetime = table.Column<int>(nullable: false), UserSsoLifetime = table.Column<int>(type: "integer", nullable: true),
NonEditable = table.Column<bool>(nullable: false) UserCodeType = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
DeviceCodeLifetime = table.Column<int>(type: "integer", nullable: false),
NonEditable = table.Column<bool>(type: "boolean", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -85,18 +108,18 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "IdentityResources", name: "IdentityResources",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(maxLength: 200, nullable: false), Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(maxLength: 200, nullable: true), DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(maxLength: 1000, nullable: true), Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Required = table.Column<bool>(nullable: false), Required = table.Column<bool>(type: "boolean", nullable: false),
Emphasize = table.Column<bool>(nullable: false), Emphasize = table.Column<bool>(type: "boolean", nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(nullable: false), ShowInDiscoveryDocument = table.Column<bool>(type: "boolean", nullable: false),
Created = table.Column<DateTime>(nullable: false), Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Updated = table.Column<DateTime>(nullable: true), Updated = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
NonEditable = table.Column<bool>(nullable: false) NonEditable = table.Column<bool>(type: "boolean", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -104,19 +127,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ApiClaims", name: "ApiResourceClaims",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Type = table.Column<string>(maxLength: 200, nullable: false), ApiResourceId = table.Column<int>(type: "integer", nullable: false),
ApiResourceId = table.Column<int>(nullable: false) Type = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ApiClaims", x => x.Id); table.PrimaryKey("PK_ApiResourceClaims", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_ApiClaims_ApiResources_ApiResourceId", name: "FK_ApiResourceClaims_ApiResources_ApiResourceId",
column: x => x.ApiResourceId, column: x => x.ApiResourceId,
principalTable: "ApiResources", principalTable: "ApiResources",
principalColumn: "Id", principalColumn: "Id",
@ -124,20 +147,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ApiProperties", name: "ApiResourceProperties",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Key = table.Column<string>(maxLength: 250, nullable: false), ApiResourceId = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(maxLength: 2000, nullable: false), Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ApiResourceId = table.Column<int>(nullable: false) Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ApiProperties", x => x.Id); table.PrimaryKey("PK_ApiResourceProperties", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_ApiProperties_ApiResources_ApiResourceId", name: "FK_ApiResourceProperties_ApiResources_ApiResourceId",
column: x => x.ApiResourceId, column: x => x.ApiResourceId,
principalTable: "ApiResources", principalTable: "ApiResources",
principalColumn: "Id", principalColumn: "Id",
@ -145,24 +168,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ApiScopes", name: "ApiResourceScopes",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(maxLength: 200, nullable: false), Scope = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(maxLength: 200, nullable: true), ApiResourceId = table.Column<int>(type: "integer", nullable: false)
Description = table.Column<string>(maxLength: 1000, nullable: true),
Required = table.Column<bool>(nullable: false),
Emphasize = table.Column<bool>(nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(nullable: false),
ApiResourceId = table.Column<int>(nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ApiScopes", x => x.Id); table.PrimaryKey("PK_ApiResourceScopes", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_ApiScopes_ApiResources_ApiResourceId", name: "FK_ApiResourceScopes_ApiResources_ApiResourceId",
column: x => x.ApiResourceId, column: x => x.ApiResourceId,
principalTable: "ApiResources", principalTable: "ApiResources",
principalColumn: "Id", principalColumn: "Id",
@ -170,38 +188,79 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ApiSecrets", name: "ApiResourceSecrets",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Description = table.Column<string>(maxLength: 1000, nullable: true), ApiResourceId = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(maxLength: 4000, nullable: false), Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Expiration = table.Column<DateTime>(nullable: true), Value = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
Type = table.Column<string>(maxLength: 250, nullable: false), Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Created = table.Column<DateTime>(nullable: false), Type = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ApiResourceId = table.Column<int>(nullable: false) Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ApiSecrets", x => x.Id); table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_ApiSecrets_ApiResources_ApiResourceId", name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId",
column: x => x.ApiResourceId, column: x => x.ApiResourceId,
principalTable: "ApiResources", principalTable: "ApiResources",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "ApiScopeClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ScopeId = table.Column<int>(type: "integer", nullable: false),
Type = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeClaims_ApiScopes_ScopeId",
column: x => x.ScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiScopeProperties",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ScopeId = table.Column<int>(type: "integer", nullable: false),
Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeProperties_ApiScopes_ScopeId",
column: x => x.ScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ClientClaims", name: "ClientClaims",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Type = table.Column<string>(maxLength: 250, nullable: false), Type = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(maxLength: 250, nullable: false), Value = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -218,10 +277,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientCorsOrigins", name: "ClientCorsOrigins",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Origin = table.Column<string>(maxLength: 150, nullable: false), Origin = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -238,10 +297,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientGrantTypes", name: "ClientGrantTypes",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
GrantType = table.Column<string>(maxLength: 250, nullable: false), GrantType = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -258,10 +317,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientIdPRestrictions", name: "ClientIdPRestrictions",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Provider = table.Column<string>(maxLength: 200, nullable: false), Provider = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -278,10 +337,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientPostLogoutRedirectUris", name: "ClientPostLogoutRedirectUris",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PostLogoutRedirectUri = table.Column<string>(maxLength: 2000, nullable: false), PostLogoutRedirectUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -298,11 +357,11 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientProperties", name: "ClientProperties",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Key = table.Column<string>(maxLength: 250, nullable: false), ClientId = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(maxLength: 2000, nullable: false), Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ClientId = table.Column<int>(nullable: false) Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -319,10 +378,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientRedirectUris", name: "ClientRedirectUris",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RedirectUri = table.Column<string>(maxLength: 2000, nullable: false), RedirectUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -339,10 +398,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientScopes", name: "ClientScopes",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Scope = table.Column<string>(maxLength: 200, nullable: false), Scope = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ClientId = table.Column<int>(nullable: false) ClientId = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -359,14 +418,14 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientSecrets", name: "ClientSecrets",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Description = table.Column<string>(maxLength: 2000, nullable: true), ClientId = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(maxLength: 4000, nullable: false), Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
Expiration = table.Column<DateTime>(nullable: true), Value = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
Type = table.Column<string>(maxLength: 250, nullable: false), Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Created = table.Column<DateTime>(nullable: false), Type = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ClientId = table.Column<int>(nullable: false) Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -380,19 +439,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "IdentityClaims", name: "IdentityResourceClaims",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Type = table.Column<string>(maxLength: 200, nullable: false), IdentityResourceId = table.Column<int>(type: "integer", nullable: false),
IdentityResourceId = table.Column<int>(nullable: false) Type = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_IdentityClaims", x => x.Id); table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_IdentityClaims_IdentityResources_IdentityResourceId", name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId",
column: x => x.IdentityResourceId, column: x => x.IdentityResourceId,
principalTable: "IdentityResources", principalTable: "IdentityResources",
principalColumn: "Id", principalColumn: "Id",
@ -400,54 +459,34 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "IdentityProperties", name: "IdentityResourceProperties",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Key = table.Column<string>(maxLength: 250, nullable: false), IdentityResourceId = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(maxLength: 2000, nullable: false), Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
IdentityResourceId = table.Column<int>(nullable: false) Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_IdentityProperties", x => x.Id); table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_IdentityProperties_IdentityResources_IdentityResourceId", name: "FK_IdentityResourceProperties_IdentityResources_IdentityResour~",
column: x => x.IdentityResourceId, column: x => x.IdentityResourceId,
principalTable: "IdentityResources", principalTable: "IdentityResources",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "ApiScopeClaims",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Type = table.Column<string>(maxLength: 200, nullable: false),
ApiScopeId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeClaims_ApiScopes_ApiScopeId",
column: x => x.ApiScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ApiClaims_ApiResourceId", name: "IX_ApiResourceClaims_ApiResourceId",
table: "ApiClaims", table: "ApiResourceClaims",
column: "ApiResourceId"); column: "ApiResourceId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ApiProperties_ApiResourceId", name: "IX_ApiResourceProperties_ApiResourceId",
table: "ApiProperties", table: "ApiResourceProperties",
column: "ApiResourceId"); column: "ApiResourceId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
@ -457,26 +496,31 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ApiScopeClaims_ApiScopeId", name: "IX_ApiResourceScopes_ApiResourceId",
table: "ApiScopeClaims", table: "ApiResourceScopes",
column: "ApiScopeId"); column: "ApiResourceId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ApiScopes_ApiResourceId", name: "IX_ApiResourceSecrets_ApiResourceId",
table: "ApiScopes", table: "ApiResourceSecrets",
column: "ApiResourceId"); column: "ApiResourceId");
migrationBuilder.CreateIndex(
name: "IX_ApiScopeClaims_ScopeId",
table: "ApiScopeClaims",
column: "ScopeId");
migrationBuilder.CreateIndex(
name: "IX_ApiScopeProperties_ScopeId",
table: "ApiScopeProperties",
column: "ScopeId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ApiScopes_Name", name: "IX_ApiScopes_Name",
table: "ApiScopes", table: "ApiScopes",
column: "Name", column: "Name",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiSecrets_ApiResourceId",
table: "ApiSecrets",
column: "ApiResourceId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ClientClaims_ClientId", name: "IX_ClientClaims_ClientId",
table: "ClientClaims", table: "ClientClaims",
@ -529,13 +573,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
column: "ClientId"); column: "ClientId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_IdentityClaims_IdentityResourceId", name: "IX_IdentityResourceClaims_IdentityResourceId",
table: "IdentityClaims", table: "IdentityResourceClaims",
column: "IdentityResourceId"); column: "IdentityResourceId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_IdentityProperties_IdentityResourceId", name: "IX_IdentityResourceProperties_IdentityResourceId",
table: "IdentityProperties", table: "IdentityResourceProperties",
column: "IdentityResourceId"); column: "IdentityResourceId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
@ -548,16 +592,22 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ApiClaims"); name: "ApiResourceClaims");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ApiProperties"); name: "ApiResourceProperties");
migrationBuilder.DropTable(
name: "ApiResourceScopes");
migrationBuilder.DropTable(
name: "ApiResourceSecrets");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ApiScopeClaims"); name: "ApiScopeClaims");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ApiSecrets"); name: "ApiScopeProperties");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ClientClaims"); name: "ClientClaims");
@ -587,10 +637,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
name: "ClientSecrets"); name: "ClientSecrets");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "IdentityClaims"); name: "IdentityResourceClaims");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "IdentityProperties"); name: "IdentityResourceProperties");
migrationBuilder.DropTable(
name: "ApiResources");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ApiScopes"); name: "ApiScopes");
@ -600,9 +653,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "IdentityResources"); name: "IdentityResources");
migrationBuilder.DropTable(
name: "ApiResources");
} }
} }
} }

View File

@ -15,9 +15,9 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{ {
@ -26,16 +26,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AllowedAccessTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("Created") b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<string>("DisplayName") b.Property<string>("DisplayName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -45,12 +49,15 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("NonEditable") b.Property<bool>("NonEditable")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated") b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
@ -74,14 +81,14 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId"); b.HasIndex("ApiResourceId");
b.ToTable("ApiClaims"); b.ToTable("ApiResourceClaims");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
@ -96,22 +103,22 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId"); b.HasIndex("ApiResourceId");
b.ToTable("ApiProperties"); b.ToTable("ApiResourceProperties");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -121,21 +128,80 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<int>("ApiResourceId") b.Property<int>("ApiResourceId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Scope")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceSecrets");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName") b.Property<string>("DisplayName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Emphasize") b.Property<bool>("Emphasize")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Required") b.Property<bool>("Required")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -145,8 +211,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.HasIndex("Name") b.HasIndex("Name")
.IsUnique(); .IsUnique();
@ -160,56 +224,46 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiScopeId") b.Property<int>("ScopeId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiScopeId"); b.HasIndex("ScopeId");
b.ToTable("ApiScopeClaims"); b.ToTable("ApiScopeClaims");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId") b.Property<string>("Key")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasColumnType("character varying(1000)")
.HasMaxLength(1000);
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<int>("ScopeId")
.HasColumnType("integer");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(4000)") .HasMaxLength(2000)
.HasMaxLength(4000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApiResourceId"); b.HasIndex("ScopeId");
b.ToTable("ApiSecrets"); b.ToTable("ApiScopeProperties");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
@ -240,6 +294,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<bool>("AllowRememberConsent") b.Property<bool>("AllowRememberConsent")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("AllowedIdentityTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<bool>("AlwaysIncludeUserClaimsInIdToken") b.Property<bool>("AlwaysIncludeUserClaimsInIdToken")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -253,25 +311,25 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("BackChannelLogoutUri") b.Property<string>("BackChannelLogoutUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<string>("ClientClaimsPrefix") b.Property<string>("ClientClaimsPrefix")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientId") b.Property<string>("ClientId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientName") b.Property<string>("ClientName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientUri") b.Property<string>("ClientUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<int?>("ConsentLifetime") b.Property<int?>("ConsentLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -280,8 +338,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<int>("DeviceCodeLifetime") b.Property<int>("DeviceCodeLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -296,8 +354,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("FrontChannelLogoutUri") b.Property<string>("FrontChannelLogoutUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<int>("IdentityTokenLifetime") b.Property<int>("IdentityTokenLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -309,20 +367,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("LogoUri") b.Property<string>("LogoUri")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<bool>("NonEditable") b.Property<bool>("NonEditable")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("PairWiseSubjectSalt") b.Property<string>("PairWiseSubjectSalt")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ProtocolType") b.Property<string>("ProtocolType")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<int>("RefreshTokenExpiration") b.Property<int>("RefreshTokenExpiration")
.HasColumnType("integer"); .HasColumnType("integer");
@ -339,6 +397,9 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<bool>("RequirePkce") b.Property<bool>("RequirePkce")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("RequireRequestObject")
.HasColumnType("boolean");
b.Property<int>("SlidingRefreshTokenLifetime") b.Property<int>("SlidingRefreshTokenLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -349,8 +410,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("UserCodeType") b.Property<string>("UserCodeType")
.HasColumnType("character varying(100)") .HasMaxLength(100)
.HasMaxLength(100); .HasColumnType("character varying(100)");
b.Property<int?>("UserSsoLifetime") b.Property<int?>("UserSsoLifetime")
.HasColumnType("integer"); .HasColumnType("integer");
@ -375,13 +436,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.HasKey("Id"); b.HasKey("Id");
@ -402,8 +463,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Origin") b.Property<string>("Origin")
.IsRequired() .IsRequired()
.HasColumnType("character varying(150)") .HasMaxLength(150)
.HasMaxLength(150); .HasColumnType("character varying(150)");
b.HasKey("Id"); b.HasKey("Id");
@ -424,8 +485,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("GrantType") b.Property<string>("GrantType")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.HasKey("Id"); b.HasKey("Id");
@ -446,8 +507,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Provider") b.Property<string>("Provider")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
@ -468,8 +529,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("PostLogoutRedirectUri") b.Property<string>("PostLogoutRedirectUri")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
@ -490,13 +551,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
@ -517,8 +578,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("RedirectUri") b.Property<string>("RedirectUri")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
@ -539,8 +600,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Scope") b.Property<string>("Scope")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("Id"); b.HasKey("Id");
@ -563,21 +624,21 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.Property<DateTime?>("Expiration") b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(4000)") .HasMaxLength(4000)
.HasMaxLength(4000); .HasColumnType("character varying(4000)");
b.HasKey("Id"); b.HasKey("Id");
@ -586,28 +647,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.ToTable("ClientSecrets"); b.ToTable("ClientSecrets");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("character varying(200)")
.HasMaxLength(200);
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -619,12 +658,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("character varying(1000)") .HasMaxLength(1000)
.HasMaxLength(1000); .HasColumnType("character varying(1000)");
b.Property<string>("DisplayName") b.Property<string>("DisplayName")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("Emphasize") b.Property<bool>("Emphasize")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -634,8 +673,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<bool>("NonEditable") b.Property<bool>("NonEditable")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -657,6 +696,28 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.ToTable("IdentityResources"); b.ToTable("IdentityResources");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityResourceClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -669,19 +730,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("character varying(250)") .HasMaxLength(250)
.HasMaxLength(250); .HasColumnType("character varying(250)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
.HasColumnType("character varying(2000)") .HasMaxLength(2000)
.HasMaxLength(2000); .HasColumnType("character varying(2000)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IdentityResourceId"); b.HasIndex("IdentityResourceId");
b.ToTable("IdentityProperties"); b.ToTable("IdentityResourceProperties");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b =>
@ -691,6 +752,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
@ -700,33 +763,52 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{ {
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Scopes") .WithMany("Scopes")
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "ApiScope")
.WithMany("UserClaims")
.HasForeignKey("ApiScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b =>
{ {
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Secrets") .WithMany("Secrets")
.HasForeignKey("ApiResourceId") .HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("UserClaims")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("Properties")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b =>
@ -736,6 +818,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b =>
@ -745,6 +829,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b =>
@ -754,6 +840,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b =>
@ -763,6 +851,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
@ -772,6 +862,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b =>
@ -781,6 +873,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b =>
@ -790,6 +884,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b =>
@ -799,6 +895,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b =>
@ -808,15 +906,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{ {
b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource")
.WithMany("UserClaims") .WithMany("UserClaims")
.HasForeignKey("IdentityResourceId") .HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("IdentityResource");
}); });
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
@ -826,6 +928,54 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
.HasForeignKey("IdentityResourceId") .HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("IdentityResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{
b.Navigation("Properties");
b.Navigation("Scopes");
b.Navigation("Secrets");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
{
b.Navigation("AllowedCorsOrigins");
b.Navigation("AllowedGrantTypes");
b.Navigation("AllowedScopes");
b.Navigation("Claims");
b.Navigation("ClientSecrets");
b.Navigation("IdentityProviderRestrictions");
b.Navigation("PostLogoutRedirectUris");
b.Navigation("Properties");
b.Navigation("RedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -7,51 +7,59 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase
{ {
[DbContext(typeof(IdentityDatabase))] [DbContext(typeof(IdentityDatabase))]
[Migration("20200526235424_Initial")] [Migration("20210216205030_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{ {
b.Property<string>("UserCode") b.Property<string>("UserCode")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientId") b.Property<string>("ClientId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Data") b.Property<string>("Data")
.IsRequired() .IsRequired()
.HasColumnType("character varying(50000)") .HasMaxLength(50000)
.HasMaxLength(50000); .HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DeviceCode") b.Property<string>("DeviceCode")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration") b.Property<DateTime?>("Expiration")
.IsRequired() .IsRequired()
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId") b.Property<string>("SubjectId")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("UserCode"); b.HasKey("UserCode");
@ -66,33 +74,44 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientId") b.Property<string>("ClientId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("timestamp without time zone");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Data") b.Property<string>("Data")
.IsRequired() .IsRequired()
.HasColumnType("character varying(50000)") .HasMaxLength(50000)
.HasMaxLength(50000); .HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration") b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId") b.Property<string>("SubjectId")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(50)") .HasMaxLength(50)
.HasMaxLength(50); .HasColumnType("character varying(50)");
b.HasKey("Key"); b.HasKey("Key");
@ -100,6 +119,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
b.HasIndex("SubjectId", "ClientId", "Type"); b.HasIndex("SubjectId", "ClientId", "Type");
b.HasIndex("SubjectId", "SessionId", "Type");
b.ToTable("PersistedGrants"); b.ToTable("PersistedGrants");
}); });
@ -116,8 +137,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Email") b.Property<string>("Email")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed") b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -129,12 +150,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail") b.Property<string>("NormalizedEmail")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName") b.Property<string>("NormalizedUserName")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<string>("OTAC") b.Property<string>("OTAC")
.HasColumnType("text"); .HasColumnType("text");
@ -158,17 +179,17 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("UserName") b.Property<string>("UserName")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedEmail") b.HasIndex("NormalizedEmail")
.HasName("EmailIndex"); .HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName") b.HasIndex("NormalizedUserName")
.IsUnique() .IsUnique()
.HasName("UserNameIndex"); .HasDatabaseName("UserNameIndex");
b.ToTable("User"); b.ToTable("User");
}); });
@ -183,18 +204,18 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<string>("NormalizedName") b.Property<string>("NormalizedName")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedName") b.HasIndex("NormalizedName")
.IsUnique() .IsUnique()
.HasName("RoleNameIndex"); .HasDatabaseName("RoleNameIndex");
b.ToTable("UserRoles"); b.ToTable("UserRoles");
}); });
@ -250,12 +271,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{ {
b.Property<string>("LoginProvider") b.Property<string>("LoginProvider")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("ProviderKey") b.Property<string>("ProviderKey")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("ProviderDisplayName") b.Property<string>("ProviderDisplayName")
.HasColumnType("text"); .HasColumnType("text");
@ -292,12 +313,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("LoginProvider") b.Property<string>("LoginProvider")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("Value") b.Property<string>("Value")
.HasColumnType("text"); .HasColumnType("text");

View File

@ -2,7 +2,7 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase
{ {
public partial class Initial : Migration public partial class Initial : Migration
{ {
@ -12,13 +12,15 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "DeviceCodes", name: "DeviceCodes",
columns: table => new columns: table => new
{ {
UserCode = table.Column<string>(maxLength: 200, nullable: false), UserCode = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DeviceCode = table.Column<string>(maxLength: 200, nullable: false), DeviceCode = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
SubjectId = table.Column<string>(maxLength: 200, nullable: true), SubjectId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
ClientId = table.Column<string>(maxLength: 200, nullable: false), SessionId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
CreationTime = table.Column<DateTime>(nullable: false), ClientId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Expiration = table.Column<DateTime>(nullable: false), Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Data = table.Column<string>(maxLength: 50000, nullable: false) CreationTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Data = table.Column<string>(type: "character varying(50000)", maxLength: 50000, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -29,13 +31,16 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "PersistedGrants", name: "PersistedGrants",
columns: table => new columns: table => new
{ {
Key = table.Column<string>(maxLength: 200, nullable: false), Key = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Type = table.Column<string>(maxLength: 50, nullable: false), Type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
SubjectId = table.Column<string>(maxLength: 200, nullable: true), SubjectId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
ClientId = table.Column<string>(maxLength: 200, nullable: false), SessionId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
CreationTime = table.Column<DateTime>(nullable: false), ClientId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Expiration = table.Column<DateTime>(nullable: true), Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Data = table.Column<string>(maxLength: 50000, nullable: false) CreationTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
ConsumedTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Data = table.Column<string>(type: "character varying(50000)", maxLength: 50000, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -46,23 +51,23 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "User", name: "User",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(nullable: false), Id = table.Column<string>(type: "text", nullable: false),
UserName = table.Column<string>(maxLength: 256, nullable: true), OTAC = table.Column<string>(type: "text", nullable: true),
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true), OTACExpires = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true), UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true), NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(nullable: false), Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
PasswordHash = table.Column<string>(nullable: true), NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
SecurityStamp = table.Column<string>(nullable: true), EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
ConcurrencyStamp = table.Column<string>(nullable: true), PasswordHash = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(nullable: true), SecurityStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false), ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
TwoFactorEnabled = table.Column<bool>(nullable: false), PhoneNumber = table.Column<string>(type: "text", nullable: true),
LockoutEnd = table.Column<DateTimeOffset>(nullable: true), PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnabled = table.Column<bool>(nullable: false), TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(nullable: false), LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
OTAC = table.Column<string>(nullable: true), LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
OTACExpires = table.Column<DateTime>(nullable: true) AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -73,10 +78,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "UserRoles", name: "UserRoles",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(nullable: false), Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(maxLength: 256, nullable: true), Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(maxLength: 256, nullable: true), NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true) ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -87,11 +92,11 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "UserClaim", name: "UserClaim",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(nullable: false), UserId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(nullable: true), ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(nullable: true) ClaimValue = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -108,10 +113,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "UserLogin", name: "UserLogin",
columns: table => new columns: table => new
{ {
LoginProvider = table.Column<string>(maxLength: 128, nullable: false), LoginProvider = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
ProviderKey = table.Column<string>(maxLength: 128, nullable: false), ProviderKey = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
ProviderDisplayName = table.Column<string>(nullable: true), ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(nullable: false) UserId = table.Column<string>(type: "text", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -128,10 +133,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "UserToken", name: "UserToken",
columns: table => new columns: table => new
{ {
UserId = table.Column<string>(nullable: false), UserId = table.Column<string>(type: "text", nullable: false),
LoginProvider = table.Column<string>(maxLength: 128, nullable: false), LoginProvider = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(maxLength: 128, nullable: false), Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Value = table.Column<string>(nullable: true) Value = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -148,35 +153,35 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
name: "UserRole", name: "UserRole",
columns: table => new columns: table => new
{ {
UserId = table.Column<string>(nullable: false), UserId = table.Column<string>(type: "text", nullable: false),
RoleId = table.Column<string>(nullable: false) RoleId = table.Column<string>(type: "text", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_UserRole", x => new { x.UserId, x.RoleId }); table.PrimaryKey("PK_UserRole", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_UserRole_UserRoles_RoleId",
column: x => x.RoleId,
principalTable: "UserRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_UserRole_User_UserId", name: "FK_UserRole_User_UserId",
column: x => x.UserId, column: x => x.UserId,
principalTable: "User", principalTable: "User",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserRole_UserRoles_RoleId",
column: x => x.RoleId,
principalTable: "UserRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "UserRoleClaim", name: "UserRoleClaim",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<string>(nullable: false), RoleId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(nullable: true), ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(nullable: true) ClaimValue = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -210,6 +215,11 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
table: "PersistedGrants", table: "PersistedGrants",
columns: new[] { "SubjectId", "ClientId", "Type" }); columns: new[] { "SubjectId", "ClientId", "Type" });
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
table: "PersistedGrants",
columns: new[] { "SubjectId", "SessionId", "Type" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "EmailIndex", name: "EmailIndex",
table: "User", table: "User",

View File

@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase
{ {
[DbContext(typeof(IdentityDatabase))] [DbContext(typeof(IdentityDatabase))]
partial class IdentityDatabaseModelSnapshot : ModelSnapshot partial class IdentityDatabaseModelSnapshot : ModelSnapshot
@ -15,41 +15,49 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{ {
b.Property<string>("UserCode") b.Property<string>("UserCode")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientId") b.Property<string>("ClientId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Data") b.Property<string>("Data")
.IsRequired() .IsRequired()
.HasColumnType("character varying(50000)") .HasMaxLength(50000)
.HasMaxLength(50000); .HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DeviceCode") b.Property<string>("DeviceCode")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration") b.Property<DateTime?>("Expiration")
.IsRequired() .IsRequired()
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId") b.Property<string>("SubjectId")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.HasKey("UserCode"); b.HasKey("UserCode");
@ -64,33 +72,44 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("ClientId") b.Property<string>("ClientId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("timestamp without time zone");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("Data") b.Property<string>("Data")
.IsRequired() .IsRequired()
.HasColumnType("character varying(50000)") .HasMaxLength(50000)
.HasMaxLength(50000); .HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration") b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId") b.Property<string>("SubjectId")
.HasColumnType("character varying(200)") .HasMaxLength(200)
.HasMaxLength(200); .HasColumnType("character varying(200)");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasColumnType("character varying(50)") .HasMaxLength(50)
.HasMaxLength(50); .HasColumnType("character varying(50)");
b.HasKey("Key"); b.HasKey("Key");
@ -98,6 +117,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
b.HasIndex("SubjectId", "ClientId", "Type"); b.HasIndex("SubjectId", "ClientId", "Type");
b.HasIndex("SubjectId", "SessionId", "Type");
b.ToTable("PersistedGrants"); b.ToTable("PersistedGrants");
}); });
@ -114,8 +135,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Email") b.Property<string>("Email")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed") b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -127,12 +148,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail") b.Property<string>("NormalizedEmail")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName") b.Property<string>("NormalizedUserName")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<string>("OTAC") b.Property<string>("OTAC")
.HasColumnType("text"); .HasColumnType("text");
@ -156,17 +177,17 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("UserName") b.Property<string>("UserName")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedEmail") b.HasIndex("NormalizedEmail")
.HasName("EmailIndex"); .HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName") b.HasIndex("NormalizedUserName")
.IsUnique() .IsUnique()
.HasName("UserNameIndex"); .HasDatabaseName("UserNameIndex");
b.ToTable("User"); b.ToTable("User");
}); });
@ -181,18 +202,18 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.Property<string>("NormalizedName") b.Property<string>("NormalizedName")
.HasColumnType("character varying(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("character varying(256)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedName") b.HasIndex("NormalizedName")
.IsUnique() .IsUnique()
.HasName("RoleNameIndex"); .HasDatabaseName("RoleNameIndex");
b.ToTable("UserRoles"); b.ToTable("UserRoles");
}); });
@ -248,12 +269,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{ {
b.Property<string>("LoginProvider") b.Property<string>("LoginProvider")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("ProviderKey") b.Property<string>("ProviderKey")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("ProviderDisplayName") b.Property<string>("ProviderDisplayName")
.HasColumnType("text"); .HasColumnType("text");
@ -290,12 +311,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("LoginProvider") b.Property<string>("LoginProvider")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("character varying(128)") .HasMaxLength(128)
.HasMaxLength(128); .HasColumnType("character varying(128)");
b.Property<string>("Value") b.Property<string>("Value")
.HasColumnType("text"); .HasColumnType("text");

View File

@ -1,6 +1,5 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Kyoo; using Kyoo;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@ -11,21 +10,21 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal namespace Kyoo.Models.DatabaseMigrations.Internal
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(DatabaseContext))]
[Migration("20210128212212_Initial")] [Migration("20210317220956_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:Enum:item_type", "show,movie,collection") .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
.HasAnnotation("Npgsql:Enum:status", "finished,airing,planned,unknown") .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
.HasAnnotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,font") .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "font" })
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.CollectionDE", b => modelBuilder.Entity("Kyoo.Models.Collection", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -51,23 +50,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.IsUnique(); .IsUnique();
b.ToTable("Collections"); b.ToTable("Collections");
b.HasDiscriminator();
});
modelBuilder.Entity("Kyoo.Models.CollectionLink", b =>
{
b.Property<int>("ParentID")
.HasColumnType("integer");
b.Property<int>("ChildID")
.HasColumnType("integer");
b.HasKey("ParentID", "ChildID");
b.HasIndex("ChildID");
b.ToTable("CollectionLinks");
}); });
modelBuilder.Entity("Kyoo.Models.Episode", b => modelBuilder.Entity("Kyoo.Models.Episode", b =>
@ -89,9 +71,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Poster")
.HasColumnType("text");
b.Property<DateTime?>("ReleaseDate") b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
@ -107,6 +86,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<int>("ShowID") b.Property<int>("ShowID")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Thumb")
.HasColumnType("text");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("text");
@ -120,7 +102,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Episodes"); b.ToTable("Episodes");
}); });
modelBuilder.Entity("Kyoo.Models.GenreDE", b => modelBuilder.Entity("Kyoo.Models.Genre", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -140,26 +122,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.IsUnique(); .IsUnique();
b.ToTable("Genres"); b.ToTable("Genres");
b.HasDiscriminator();
}); });
modelBuilder.Entity("Kyoo.Models.GenreLink", b => modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Property<int>("ParentID")
.HasColumnType("integer");
b.Property<int>("ChildID")
.HasColumnType("integer");
b.HasKey("ParentID", "ChildID");
b.HasIndex("ChildID");
b.ToTable("GenreLinks");
});
modelBuilder.Entity("Kyoo.Models.LibraryDE", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -169,7 +134,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("text");
b.Property<IEnumerable<string>>("Paths") b.Property<string[]>("Paths")
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<string>("Slug") b.Property<string>("Slug")
@ -182,39 +147,81 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.IsUnique(); .IsUnique();
b.ToTable("Libraries"); b.ToTable("Libraries");
b.HasDiscriminator();
}); });
modelBuilder.Entity("Kyoo.Models.LibraryLink", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{ {
b.Property<int>("ID") b.Property<int>("FirstID")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int?>("CollectionID")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int>("LibraryID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int?>("ShowID") b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Collection, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("integer");
b.HasKey("ID"); b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasIndex("CollectionID"); b.HasKey("FirstID", "SecondID");
b.HasIndex("ShowID"); b.HasIndex("SecondID");
b.HasIndex("LibraryID", "CollectionID") b.ToTable("Link<Library, Collection>");
.IsUnique(); });
b.HasIndex("LibraryID", "ShowID") modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b =>
.IsUnique(); {
b.Property<int>("FirstID")
.HasColumnType("integer");
b.ToTable("LibraryLinks"); b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, ProviderID>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Show, Genre>");
}); });
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
@ -338,21 +345,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Providers"); b.ToTable("Providers");
}); });
modelBuilder.Entity("Kyoo.Models.ProviderLink", b =>
{
b.Property<int>("ParentID")
.HasColumnType("integer");
b.Property<int>("ChildID")
.HasColumnType("integer");
b.HasKey("ParentID", "ChildID");
b.HasIndex("ChildID");
b.ToTable("ProviderLinks");
});
modelBuilder.Entity("Kyoo.Models.Season", b => modelBuilder.Entity("Kyoo.Models.Season", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
@ -386,14 +378,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Seasons"); b.ToTable("Seasons");
}); });
modelBuilder.Entity("Kyoo.Models.ShowDE", b => modelBuilder.Entity("Kyoo.Models.Show", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<IEnumerable<string>>("Aliases") b.Property<string[]>("Aliases")
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<string>("Backdrop") b.Property<string>("Backdrop")
@ -444,8 +436,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.HasIndex("StudioID"); b.HasIndex("StudioID");
b.ToTable("Shows"); b.ToTable("Shows");
b.HasDiscriminator();
}); });
modelBuilder.Entity("Kyoo.Models.Studio", b => modelBuilder.Entity("Kyoo.Models.Studio", b =>
@ -501,76 +491,130 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("TrackIndex")
.HasColumnType("integer");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("EpisodeID"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique();
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });
modelBuilder.Entity("Kyoo.Models.CollectionLink", b =>
{
b.HasOne("Kyoo.Models.ShowDE", "Child")
.WithMany("CollectionLinks")
.HasForeignKey("ChildID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.CollectionDE", "Parent")
.WithMany("Links")
.HasForeignKey("ParentID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Kyoo.Models.Episode", b => modelBuilder.Entity("Kyoo.Models.Episode", b =>
{ {
b.HasOne("Kyoo.Models.Season", "Season") b.HasOne("Kyoo.Models.Season", "Season")
.WithMany("Episodes") .WithMany("Episodes")
.HasForeignKey("SeasonID"); .HasForeignKey("SeasonID");
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Episodes") .WithMany("Episodes")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Season");
b.Navigation("Show");
}); });
modelBuilder.Entity("Kyoo.Models.GenreLink", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{ {
b.HasOne("Kyoo.Models.GenreDE", "Child") b.HasOne("Kyoo.Models.Collection", "First")
.WithMany("Links") .WithMany("ShowLinks")
.HasForeignKey("ChildID") .HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ShowDE", "Parent") b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("CollectionLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("CollectionLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Collection", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ProviderLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.ProviderID", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.HasOne("Kyoo.Models.Show", "First")
.WithMany("GenreLinks") .WithMany("GenreLinks")
.HasForeignKey("ParentID") .HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Kyoo.Models.LibraryLink", b =>
{
b.HasOne("Kyoo.Models.CollectionDE", "Collection")
.WithMany("LibraryLinks")
.HasForeignKey("CollectionID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.LibraryDE", "Library")
.WithMany("Links")
.HasForeignKey("LibraryID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Genre", "Second")
.WithMany("LibraryLinks") .WithMany("ShowLinks")
.HasForeignKey("ShowID") .HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
}); });
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
@ -586,7 +630,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ProviderID", "Provider") b.HasOne("Kyoo.Models.ProviderID", "Provider")
.WithMany() .WithMany("MetadataLinks")
.HasForeignKey("ProviderID") .HasForeignKey("ProviderID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -596,10 +640,20 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("SeasonID") .HasForeignKey("SeasonID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("ExternalIDs") .WithMany("ExternalIDs")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.Navigation("Episode");
b.Navigation("People");
b.Navigation("Provider");
b.Navigation("Season");
b.Navigation("Show");
}); });
modelBuilder.Entity("Kyoo.Models.PeopleRole", b => modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
@ -610,42 +664,35 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("People") .WithMany("People")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
});
modelBuilder.Entity("Kyoo.Models.ProviderLink", b => b.Navigation("People");
{
b.HasOne("Kyoo.Models.ProviderID", "Child")
.WithMany()
.HasForeignKey("ChildID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.LibraryDE", "Parent") b.Navigation("Show");
.WithMany("ProviderLinks")
.HasForeignKey("ParentID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("Kyoo.Models.Season", b => modelBuilder.Entity("Kyoo.Models.Season", b =>
{ {
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Seasons") .WithMany("Seasons")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Show");
}); });
modelBuilder.Entity("Kyoo.Models.ShowDE", b => modelBuilder.Entity("Kyoo.Models.Show", b =>
{ {
b.HasOne("Kyoo.Models.Studio", "Studio") b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany() .WithMany("Shows")
.HasForeignKey("StudioID"); .HasForeignKey("StudioID");
b.Navigation("Studio");
}); });
modelBuilder.Entity("Kyoo.Models.Track", b => modelBuilder.Entity("Kyoo.Models.Track", b =>
@ -655,6 +702,79 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("EpisodeID") .HasForeignKey("EpisodeID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Episode");
});
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Navigation("LibraryLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Tracks");
});
modelBuilder.Entity("Kyoo.Models.Genre", b =>
{
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("ProviderLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.People", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Roles");
});
modelBuilder.Entity("Kyoo.Models.ProviderID", b =>
{
b.Navigation("LibraryLinks");
b.Navigation("MetadataLinks");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
b.Navigation("GenreLinks");
b.Navigation("LibraryLinks");
b.Navigation("People");
b.Navigation("Seasons");
});
modelBuilder.Entity("Kyoo.Models.Studio", b =>
{
b.Navigation("Shows");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -17,12 +17,12 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Collections", name: "Collections",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(nullable: true), Name = table.Column<string>(type: "text", nullable: true),
Poster = table.Column<string>(nullable: true), Poster = table.Column<string>(type: "text", nullable: true),
Overview = table.Column<string>(nullable: true) Overview = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -33,10 +33,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Genres", name: "Genres",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(nullable: true) Name = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -47,10 +47,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Libraries", name: "Libraries",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(nullable: true), Name = table.Column<string>(type: "text", nullable: true),
Paths = table.Column<string[]>(type: "text[]", nullable: true) Paths = table.Column<string[]>(type: "text[]", nullable: true)
}, },
constraints: table => constraints: table =>
@ -62,11 +62,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "People", name: "People",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(nullable: true), Name = table.Column<string>(type: "text", nullable: true),
Poster = table.Column<string>(nullable: true) Poster = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -77,11 +77,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Providers", name: "Providers",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(nullable: true), Name = table.Column<string>(type: "text", nullable: true),
Logo = table.Column<string>(nullable: true) Logo = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -92,10 +92,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Studios", name: "Studios",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(nullable: true) Name = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -103,49 +103,73 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ProviderLinks", name: "Link<Library, Collection>",
columns: table => new columns: table => new
{ {
ParentID = table.Column<int>(nullable: false), FirstID = table.Column<int>(type: "integer", nullable: false),
ChildID = table.Column<int>(nullable: false) SecondID = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ProviderLinks", x => new { x.ParentID, x.ChildID }); table.PrimaryKey("PK_Link<Library, Collection>", x => new { x.FirstID, x.SecondID });
table.ForeignKey( table.ForeignKey(
name: "FK_ProviderLinks_Providers_ChildID", name: "FK_Link<Library, Collection>_Collections_SecondID",
column: x => x.ChildID, column: x => x.SecondID,
principalTable: "Providers", principalTable: "Collections",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_ProviderLinks_Libraries_ParentID", name: "FK_Link<Library, Collection>_Libraries_FirstID",
column: x => x.ParentID, column: x => x.FirstID,
principalTable: "Libraries", principalTable: "Libraries",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "Link<Library, ProviderID>",
columns: table => new
{
FirstID = table.Column<int>(type: "integer", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<Library, ProviderID>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<Library, ProviderID>_Libraries_FirstID",
column: x => x.FirstID,
principalTable: "Libraries",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<Library, ProviderID>_Providers_SecondID",
column: x => x.SecondID,
principalTable: "Providers",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Shows", name: "Shows",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(nullable: false), Slug = table.Column<string>(type: "text", nullable: false),
Title = table.Column<string>(nullable: true), Title = table.Column<string>(type: "text", nullable: true),
Aliases = table.Column<string[]>(type: "text[]", nullable: true), Aliases = table.Column<string[]>(type: "text[]", nullable: true),
Path = table.Column<string>(nullable: true), Path = table.Column<string>(type: "text", nullable: true),
Overview = table.Column<string>(nullable: true), Overview = table.Column<string>(type: "text", nullable: true),
Status = table.Column<int>(nullable: true), Status = table.Column<int>(type: "integer", nullable: true),
TrailerUrl = table.Column<string>(nullable: true), TrailerUrl = table.Column<string>(type: "text", nullable: true),
StartYear = table.Column<int>(nullable: true), StartYear = table.Column<int>(type: "integer", nullable: true),
EndYear = table.Column<int>(nullable: true), EndYear = table.Column<int>(type: "integer", nullable: true),
Poster = table.Column<string>(nullable: true), Poster = table.Column<string>(type: "text", nullable: true),
Logo = table.Column<string>(nullable: true), Logo = table.Column<string>(type: "text", nullable: true),
Backdrop = table.Column<string>(nullable: true), Backdrop = table.Column<string>(type: "text", nullable: true),
IsMovie = table.Column<bool>(nullable: false), IsMovie = table.Column<bool>(type: "boolean", nullable: false),
StudioID = table.Column<int>(nullable: true) StudioID = table.Column<int>(type: "integer", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -159,81 +183,72 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "CollectionLinks", name: "Link<Collection, Show>",
columns: table => new columns: table => new
{ {
ParentID = table.Column<int>(nullable: false), FirstID = table.Column<int>(type: "integer", nullable: false),
ChildID = table.Column<int>(nullable: false) SecondID = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_CollectionLinks", x => new { x.ParentID, x.ChildID }); table.PrimaryKey("PK_Link<Collection, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey( table.ForeignKey(
name: "FK_CollectionLinks_Shows_ChildID", name: "FK_Link<Collection, Show>_Collections_FirstID",
column: x => x.ChildID, column: x => x.FirstID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CollectionLinks_Collections_ParentID",
column: x => x.ParentID,
principalTable: "Collections", principalTable: "Collections",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "GenreLinks",
columns: table => new
{
ParentID = table.Column<int>(nullable: false),
ChildID = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GenreLinks", x => new { x.ParentID, x.ChildID });
table.ForeignKey( table.ForeignKey(
name: "FK_GenreLinks_Genres_ChildID", name: "FK_Link<Collection, Show>_Shows_SecondID",
column: x => x.ChildID, column: x => x.SecondID,
principalTable: "Genres",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GenreLinks_Shows_ParentID",
column: x => x.ParentID,
principalTable: "Shows", principalTable: "Shows",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "LibraryLinks", name: "Link<Library, Show>",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) FirstID = table.Column<int>(type: "integer", nullable: false),
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), SecondID = table.Column<int>(type: "integer", nullable: false)
LibraryID = table.Column<int>(nullable: false),
ShowID = table.Column<int>(nullable: true),
CollectionID = table.Column<int>(nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_LibraryLinks", x => x.ID); table.PrimaryKey("PK_Link<Library, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey( table.ForeignKey(
name: "FK_LibraryLinks_Collections_CollectionID", name: "FK_Link<Library, Show>_Libraries_FirstID",
column: x => x.CollectionID, column: x => x.FirstID,
principalTable: "Collections",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LibraryLinks_Libraries_LibraryID",
column: x => x.LibraryID,
principalTable: "Libraries", principalTable: "Libraries",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_LibraryLinks_Shows_ShowID", name: "FK_Link<Library, Show>_Shows_SecondID",
column: x => x.ShowID, column: x => x.SecondID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link<Show, Genre>",
columns: table => new
{
FirstID = table.Column<int>(type: "integer", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<Show, Genre>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<Show, Genre>_Genres_SecondID",
column: x => x.SecondID,
principalTable: "Genres",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<Show, Genre>_Shows_FirstID",
column: x => x.FirstID,
principalTable: "Shows", principalTable: "Shows",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -243,12 +258,12 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "PeopleRoles", name: "PeopleRoles",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PeopleID = table.Column<int>(nullable: false), PeopleID = table.Column<int>(type: "integer", nullable: false),
ShowID = table.Column<int>(nullable: false), ShowID = table.Column<int>(type: "integer", nullable: false),
Role = table.Column<string>(nullable: true), Role = table.Column<string>(type: "text", nullable: true),
Type = table.Column<string>(nullable: true) Type = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -271,14 +286,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Seasons", name: "Seasons",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ShowID = table.Column<int>(nullable: false), ShowID = table.Column<int>(type: "integer", nullable: false),
SeasonNumber = table.Column<int>(nullable: false), SeasonNumber = table.Column<int>(type: "integer", nullable: false),
Title = table.Column<string>(nullable: true), Title = table.Column<string>(type: "text", nullable: true),
Overview = table.Column<string>(nullable: true), Overview = table.Column<string>(type: "text", nullable: true),
Year = table.Column<int>(nullable: true), Year = table.Column<int>(type: "integer", nullable: true),
Poster = table.Column<string>(nullable: true) Poster = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -295,19 +310,19 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Episodes", name: "Episodes",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ShowID = table.Column<int>(nullable: false), ShowID = table.Column<int>(type: "integer", nullable: false),
SeasonID = table.Column<int>(nullable: true), SeasonID = table.Column<int>(type: "integer", nullable: true),
SeasonNumber = table.Column<int>(nullable: false), SeasonNumber = table.Column<int>(type: "integer", nullable: false),
EpisodeNumber = table.Column<int>(nullable: false), EpisodeNumber = table.Column<int>(type: "integer", nullable: false),
AbsoluteNumber = table.Column<int>(nullable: false), AbsoluteNumber = table.Column<int>(type: "integer", nullable: false),
Path = table.Column<string>(nullable: true), Path = table.Column<string>(type: "text", nullable: true),
Title = table.Column<string>(nullable: true), Thumb = table.Column<string>(type: "text", nullable: true),
Overview = table.Column<string>(nullable: true), Title = table.Column<string>(type: "text", nullable: true),
ReleaseDate = table.Column<DateTime>(nullable: true), Overview = table.Column<string>(type: "text", nullable: true),
Runtime = table.Column<int>(nullable: false), ReleaseDate = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Poster = table.Column<string>(nullable: true) Runtime = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -330,15 +345,15 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "MetadataIds", name: "MetadataIds",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProviderID = table.Column<int>(nullable: false), ProviderID = table.Column<int>(type: "integer", nullable: false),
ShowID = table.Column<int>(nullable: true), ShowID = table.Column<int>(type: "integer", nullable: true),
EpisodeID = table.Column<int>(nullable: true), EpisodeID = table.Column<int>(type: "integer", nullable: true),
SeasonID = table.Column<int>(nullable: true), SeasonID = table.Column<int>(type: "integer", nullable: true),
PeopleID = table.Column<int>(nullable: true), PeopleID = table.Column<int>(type: "integer", nullable: true),
DataID = table.Column<string>(nullable: true), DataID = table.Column<string>(type: "text", nullable: true),
Link = table.Column<string>(nullable: true) Link = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -379,17 +394,18 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Tracks", name: "Tracks",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(nullable: false) ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Title = table.Column<string>(nullable: true), EpisodeID = table.Column<int>(type: "integer", nullable: false),
Language = table.Column<string>(nullable: true), TrackIndex = table.Column<int>(type: "integer", nullable: false),
Codec = table.Column<string>(nullable: true), IsDefault = table.Column<bool>(type: "boolean", nullable: false),
Path = table.Column<string>(nullable: true), IsForced = table.Column<bool>(type: "boolean", nullable: false),
Type = table.Column<int>(nullable: false), IsExternal = table.Column<bool>(type: "boolean", nullable: false),
EpisodeID = table.Column<int>(nullable: false), Title = table.Column<string>(type: "text", nullable: true),
IsDefault = table.Column<bool>(nullable: false), Language = table.Column<string>(type: "text", nullable: true),
IsForced = table.Column<bool>(nullable: false), Codec = table.Column<string>(type: "text", nullable: true),
IsExternal = table.Column<bool>(nullable: false) Path = table.Column<string>(type: "text", nullable: true),
Type = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -402,11 +418,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_CollectionLinks_ChildID",
table: "CollectionLinks",
column: "ChildID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Collections_Slug", name: "IX_Collections_Slug",
table: "Collections", table: "Collections",
@ -424,11 +435,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_GenreLinks_ChildID",
table: "GenreLinks",
column: "ChildID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Genres_Slug", name: "IX_Genres_Slug",
table: "Genres", table: "Genres",
@ -442,26 +448,29 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_LibraryLinks_CollectionID", name: "IX_Link<Collection, Show>_SecondID",
table: "LibraryLinks", table: "Link<Collection, Show>",
column: "CollectionID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_LibraryLinks_ShowID", name: "IX_Link<Library, Collection>_SecondID",
table: "LibraryLinks", table: "Link<Library, Collection>",
column: "ShowID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_LibraryLinks_LibraryID_CollectionID", name: "IX_Link<Library, ProviderID>_SecondID",
table: "LibraryLinks", table: "Link<Library, ProviderID>",
columns: new[] { "LibraryID", "CollectionID" }, column: "SecondID");
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_LibraryLinks_LibraryID_ShowID", name: "IX_Link<Library, Show>_SecondID",
table: "LibraryLinks", table: "Link<Library, Show>",
columns: new[] { "LibraryID", "ShowID" }, column: "SecondID");
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Link<Show, Genre>_SecondID",
table: "Link<Show, Genre>",
column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MetadataIds_EpisodeID", name: "IX_MetadataIds_EpisodeID",
@ -504,11 +513,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
table: "PeopleRoles", table: "PeopleRoles",
column: "ShowID"); column: "ShowID");
migrationBuilder.CreateIndex(
name: "IX_ProviderLinks_ChildID",
table: "ProviderLinks",
column: "ChildID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Providers_Slug", name: "IX_Providers_Slug",
table: "Providers", table: "Providers",
@ -539,21 +543,28 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Tracks_EpisodeID", name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced",
table: "Tracks", table: "Tracks",
column: "EpisodeID"); columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" },
unique: true);
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "CollectionLinks"); name: "Link<Collection, Show>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "GenreLinks"); name: "Link<Library, Collection>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "LibraryLinks"); name: "Link<Library, ProviderID>");
migrationBuilder.DropTable(
name: "Link<Library, Show>");
migrationBuilder.DropTable(
name: "Link<Show, Genre>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MetadataIds"); name: "MetadataIds");
@ -561,26 +572,23 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PeopleRoles"); name: "PeopleRoles");
migrationBuilder.DropTable(
name: "ProviderLinks");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Tracks"); name: "Tracks");
migrationBuilder.DropTable(
name: "Genres");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Collections"); name: "Collections");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "People"); name: "Libraries");
migrationBuilder.DropTable(
name: "Genres");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Providers"); name: "Providers");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Libraries"); name: "People");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Episodes"); name: "Episodes");

View File

@ -1,6 +1,5 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Kyoo; using Kyoo;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@ -16,14 +15,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:Enum:item_type", "show,movie,collection") .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
.HasAnnotation("Npgsql:Enum:status", "finished,airing,planned,unknown") .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
.HasAnnotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,font") .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "font" })
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.CollectionDE", b => modelBuilder.Entity("Kyoo.Models.Collection", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -49,23 +48,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.IsUnique(); .IsUnique();
b.ToTable("Collections"); b.ToTable("Collections");
b.HasDiscriminator();
});
modelBuilder.Entity("Kyoo.Models.CollectionLink", b =>
{
b.Property<int>("ParentID")
.HasColumnType("integer");
b.Property<int>("ChildID")
.HasColumnType("integer");
b.HasKey("ParentID", "ChildID");
b.HasIndex("ChildID");
b.ToTable("CollectionLinks");
}); });
modelBuilder.Entity("Kyoo.Models.Episode", b => modelBuilder.Entity("Kyoo.Models.Episode", b =>
@ -87,9 +69,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Poster")
.HasColumnType("text");
b.Property<DateTime?>("ReleaseDate") b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
@ -105,6 +84,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<int>("ShowID") b.Property<int>("ShowID")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Thumb")
.HasColumnType("text");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("text");
@ -118,7 +100,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Episodes"); b.ToTable("Episodes");
}); });
modelBuilder.Entity("Kyoo.Models.GenreDE", b => modelBuilder.Entity("Kyoo.Models.Genre", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -138,26 +120,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.IsUnique(); .IsUnique();
b.ToTable("Genres"); b.ToTable("Genres");
b.HasDiscriminator();
}); });
modelBuilder.Entity("Kyoo.Models.GenreLink", b => modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Property<int>("ParentID")
.HasColumnType("integer");
b.Property<int>("ChildID")
.HasColumnType("integer");
b.HasKey("ParentID", "ChildID");
b.HasIndex("ChildID");
b.ToTable("GenreLinks");
});
modelBuilder.Entity("Kyoo.Models.LibraryDE", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -167,7 +132,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("text");
b.Property<IEnumerable<string>>("Paths") b.Property<string[]>("Paths")
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<string>("Slug") b.Property<string>("Slug")
@ -180,39 +145,81 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.IsUnique(); .IsUnique();
b.ToTable("Libraries"); b.ToTable("Libraries");
b.HasDiscriminator();
}); });
modelBuilder.Entity("Kyoo.Models.LibraryLink", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{ {
b.Property<int>("ID") b.Property<int>("FirstID")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int?>("CollectionID")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int>("LibraryID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int?>("ShowID") b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Collection, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("integer");
b.HasKey("ID"); b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasIndex("CollectionID"); b.HasKey("FirstID", "SecondID");
b.HasIndex("ShowID"); b.HasIndex("SecondID");
b.HasIndex("LibraryID", "CollectionID") b.ToTable("Link<Library, Collection>");
.IsUnique(); });
b.HasIndex("LibraryID", "ShowID") modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b =>
.IsUnique(); {
b.Property<int>("FirstID")
.HasColumnType("integer");
b.ToTable("LibraryLinks"); b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, ProviderID>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Show, Genre>");
}); });
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
@ -336,21 +343,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Providers"); b.ToTable("Providers");
}); });
modelBuilder.Entity("Kyoo.Models.ProviderLink", b =>
{
b.Property<int>("ParentID")
.HasColumnType("integer");
b.Property<int>("ChildID")
.HasColumnType("integer");
b.HasKey("ParentID", "ChildID");
b.HasIndex("ChildID");
b.ToTable("ProviderLinks");
});
modelBuilder.Entity("Kyoo.Models.Season", b => modelBuilder.Entity("Kyoo.Models.Season", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
@ -384,14 +376,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Seasons"); b.ToTable("Seasons");
}); });
modelBuilder.Entity("Kyoo.Models.ShowDE", b => modelBuilder.Entity("Kyoo.Models.Show", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<IEnumerable<string>>("Aliases") b.Property<string[]>("Aliases")
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<string>("Backdrop") b.Property<string>("Backdrop")
@ -442,8 +434,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.HasIndex("StudioID"); b.HasIndex("StudioID");
b.ToTable("Shows"); b.ToTable("Shows");
b.HasDiscriminator();
}); });
modelBuilder.Entity("Kyoo.Models.Studio", b => modelBuilder.Entity("Kyoo.Models.Studio", b =>
@ -499,76 +489,130 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("TrackIndex")
.HasColumnType("integer");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("EpisodeID"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique();
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });
modelBuilder.Entity("Kyoo.Models.CollectionLink", b =>
{
b.HasOne("Kyoo.Models.ShowDE", "Child")
.WithMany("CollectionLinks")
.HasForeignKey("ChildID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.CollectionDE", "Parent")
.WithMany("Links")
.HasForeignKey("ParentID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Kyoo.Models.Episode", b => modelBuilder.Entity("Kyoo.Models.Episode", b =>
{ {
b.HasOne("Kyoo.Models.Season", "Season") b.HasOne("Kyoo.Models.Season", "Season")
.WithMany("Episodes") .WithMany("Episodes")
.HasForeignKey("SeasonID"); .HasForeignKey("SeasonID");
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Episodes") .WithMany("Episodes")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Season");
b.Navigation("Show");
}); });
modelBuilder.Entity("Kyoo.Models.GenreLink", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{ {
b.HasOne("Kyoo.Models.GenreDE", "Child") b.HasOne("Kyoo.Models.Collection", "First")
.WithMany("Links") .WithMany("ShowLinks")
.HasForeignKey("ChildID") .HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ShowDE", "Parent") b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("CollectionLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("CollectionLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Collection", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ProviderLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.ProviderID", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.HasOne("Kyoo.Models.Show", "First")
.WithMany("GenreLinks") .WithMany("GenreLinks")
.HasForeignKey("ParentID") .HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Kyoo.Models.LibraryLink", b =>
{
b.HasOne("Kyoo.Models.CollectionDE", "Collection")
.WithMany("LibraryLinks")
.HasForeignKey("CollectionID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.LibraryDE", "Library")
.WithMany("Links")
.HasForeignKey("LibraryID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Genre", "Second")
.WithMany("LibraryLinks") .WithMany("ShowLinks")
.HasForeignKey("ShowID") .HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
}); });
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
@ -584,7 +628,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ProviderID", "Provider") b.HasOne("Kyoo.Models.ProviderID", "Provider")
.WithMany() .WithMany("MetadataLinks")
.HasForeignKey("ProviderID") .HasForeignKey("ProviderID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -594,10 +638,20 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("SeasonID") .HasForeignKey("SeasonID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("ExternalIDs") .WithMany("ExternalIDs")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.Navigation("Episode");
b.Navigation("People");
b.Navigation("Provider");
b.Navigation("Season");
b.Navigation("Show");
}); });
modelBuilder.Entity("Kyoo.Models.PeopleRole", b => modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
@ -608,42 +662,35 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("People") .WithMany("People")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
});
modelBuilder.Entity("Kyoo.Models.ProviderLink", b => b.Navigation("People");
{
b.HasOne("Kyoo.Models.ProviderID", "Child")
.WithMany()
.HasForeignKey("ChildID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.LibraryDE", "Parent") b.Navigation("Show");
.WithMany("ProviderLinks")
.HasForeignKey("ParentID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("Kyoo.Models.Season", b => modelBuilder.Entity("Kyoo.Models.Season", b =>
{ {
b.HasOne("Kyoo.Models.ShowDE", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Seasons") .WithMany("Seasons")
.HasForeignKey("ShowID") .HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Show");
}); });
modelBuilder.Entity("Kyoo.Models.ShowDE", b => modelBuilder.Entity("Kyoo.Models.Show", b =>
{ {
b.HasOne("Kyoo.Models.Studio", "Studio") b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany() .WithMany("Shows")
.HasForeignKey("StudioID"); .HasForeignKey("StudioID");
b.Navigation("Studio");
}); });
modelBuilder.Entity("Kyoo.Models.Track", b => modelBuilder.Entity("Kyoo.Models.Track", b =>
@ -653,6 +700,79 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("EpisodeID") .HasForeignKey("EpisodeID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Episode");
});
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Navigation("LibraryLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Tracks");
});
modelBuilder.Entity("Kyoo.Models.Genre", b =>
{
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("ProviderLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.People", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Roles");
});
modelBuilder.Entity("Kyoo.Models.ProviderID", b =>
{
b.Navigation("LibraryLinks");
b.Navigation("MetadataLinks");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
b.Navigation("GenreLinks");
b.Navigation("LibraryLinks");
b.Navigation("People");
b.Navigation("Seasons");
});
modelBuilder.Entity("Kyoo.Models.Studio", b =>
{
b.Navigation("Shows");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -1,9 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using IdentityServer4.Models; using IdentityServer4.Models;
namespace Kyoo namespace Kyoo
{ {
public class IdentityContext public static class IdentityContext
{ {
public static IEnumerable<IdentityResource> GetIdentityResources() public static IEnumerable<IdentityResource> GetIdentityResources()
{ {
@ -19,7 +20,7 @@ namespace Kyoo
{ {
return new List<Client> return new List<Client>
{ {
new Client new()
{ {
ClientId = "kyoo.webapp", ClientId = "kyoo.webapp",
@ -39,6 +40,38 @@ namespace Kyoo
}; };
} }
public static IEnumerable<ApiScope> GetScopes()
{
return new[]
{
new ApiScope
{
Name = "kyoo.read",
DisplayName = "Read only access to the API.",
},
new ApiScope
{
Name = "kyoo.write",
DisplayName = "Read and write access to the public API"
},
new ApiScope
{
Name = "kyoo.play",
DisplayName = "Allow playback of movies and episodes."
},
new ApiScope
{
Name = "kyoo.download",
DisplayName = "Allow downloading of episodes and movies from kyoo."
},
new ApiScope
{
Name = "kyoo.admin",
DisplayName = "Full access to the admin's API and the public API."
}
};
}
public static IEnumerable<ApiResource> GetApis() public static IEnumerable<ApiResource> GetApis()
{ {
return new[] return new[]
@ -46,34 +79,7 @@ namespace Kyoo
new ApiResource new ApiResource
{ {
Name = "Kyoo", Name = "Kyoo",
Scopes = Scopes = GetScopes().Select(x => x.Name).ToArray()
{
new Scope
{
Name = "kyoo.read",
DisplayName = "Read only access to the API.",
},
new Scope
{
Name = "kyoo.write",
DisplayName = "Read and write access to the public API"
},
new Scope
{
Name = "kyoo.play",
DisplayName = "Allow playback of movies and episodes."
},
new Scope
{
Name = "kyoo.download",
DisplayName = "Allow downloading of episodes and movies from kyoo."
},
new Scope
{
Name = "kyoo.admin",
DisplayName = "Full access to the admin's API and the public API."
}
}
} }
}; };
} }

View File

@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
namespace Kyoo namespace Kyoo
{ {
// The configuration's database is named ConfigurationDbContext.
public class IdentityDatabase : IdentityDbContext<User>, IPersistedGrantDbContext public class IdentityDatabase : IdentityDbContext<User>, IPersistedGrantDbContext
{ {
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions; private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;

View File

@ -1,20 +0,0 @@
namespace Kyoo.Models
{
public class CollectionLink : IResourceLink<Collection, Show>
{
public int ParentID { get; set; }
public virtual Collection Parent { get; set; }
public int ChildID { get; set; }
public virtual Show Child { get; set; }
public CollectionLink() { }
public CollectionLink(Collection parent, Show child)
{
Parent = parent;
ParentID = parent.ID;
Child = child;
ChildID = child.ID;
}
}
}

View File

@ -1,18 +0,0 @@
namespace Kyoo.Models
{
public class GenreLink : IResourceLink<Show, Genre>
{
public int ParentID { get; set; }
public virtual Show Parent { get; set; }
public int ChildID { get; set; }
public virtual Genre Child { get; set; }
public GenreLink() {}
public GenreLink(Show parent, Genre child)
{
Parent = parent;
Child = child;
}
}
}

View File

@ -1,27 +0,0 @@
namespace Kyoo.Models
{
public class LibraryLink
{
public int ID { get; set; }
public int LibraryID { get; set; }
public virtual Library Library { get; set; }
public int? ShowID { get; set; }
public virtual Show Show { get; set; }
public int? CollectionID { get; set; }
public virtual Collection Collection { get; set; }
public LibraryLink() { }
public LibraryLink(Library library, Show show)
{
Library = library;
Show = show;
}
public LibraryLink(Library library, Collection collection)
{
Library = library;
Collection = collection;
}
}
}

View File

@ -1,20 +0,0 @@
using Newtonsoft.Json;
namespace Kyoo.Models
{
public class ProviderLink : IResourceLink<Library, ProviderID>
{
[JsonIgnore] public int ParentID { get; set; }
[JsonIgnore] public virtual Library Parent { get; set; }
[JsonIgnore] public int ChildID { get; set; }
[JsonIgnore] public virtual ProviderID Child { get; set; }
public ProviderLink() { }
public ProviderLink(ProviderID child, Library parent)
{
Child = child;
Parent = parent;
}
}
}

View File

@ -1,33 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
public class CollectionDE : Collection
{
[JsonIgnore] [NotMergable] public virtual ICollection<CollectionLink> Links { get; set; }
[ExpressionRewrite(nameof(Links), nameof(CollectionLink.Child))]
public override IEnumerable<Show> Shows
{
get => Links?.Select(x => x.Child);
set => Links = value?.Select(x => new CollectionLink(this, x)).ToList();
}
[JsonIgnore] [NotMergable] public virtual ICollection<LibraryLink> LibraryLinks { get; set; }
[ExpressionRewrite(nameof(LibraryLinks), nameof(GenreLink.Child))]
public override IEnumerable<Library> Libraries
{
get => LibraryLinks?.Select(x => x.Library);
set => LibraryLinks = value?.Select(x => new LibraryLink(x, this)).ToList();
}
public CollectionDE() {}
public CollectionDE(Collection collection)
{
Utility.Assign(this, collection);
}
}
}

View File

@ -1,25 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
public class GenreDE : Genre
{
[JsonIgnore] [NotMergable] public virtual ICollection<GenreLink> Links { get; set; }
[ExpressionRewrite(nameof(Links), nameof(GenreLink.Child))]
[JsonIgnore] [NotMergable] public override IEnumerable<Show> Shows
{
get => Links?.Select(x => x.Parent);
set => Links = value?.Select(x => new GenreLink(x, this)).ToList();
}
public GenreDE() {}
public GenreDE(Genre item)
{
Utility.Assign(this, item);
}
}
}

View File

@ -1,42 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
public class LibraryDE : Library
{
[EditableRelation] [JsonIgnore] [NotMergable] public virtual ICollection<ProviderLink> ProviderLinks { get; set; }
[ExpressionRewrite(nameof(ProviderLinks), nameof(ProviderLink.Child))]
public override IEnumerable<ProviderID> Providers
{
get => ProviderLinks?.Select(x => x.Child);
set => ProviderLinks = value?.Select(x => new ProviderLink(x, this)).ToList();
}
[JsonIgnore] [NotMergable] public virtual ICollection<LibraryLink> Links { get; set; }
[ExpressionRewrite(nameof(Links), nameof(LibraryLink.Show))]
public override IEnumerable<Show> Shows
{
get => Links?.Where(x => x.Show != null).Select(x => x.Show);
set => Links = Utility.MergeLists(
value?.Select(x => new LibraryLink(this, x)),
Links?.Where(x => x.Show == null))?.ToList();
}
[ExpressionRewrite(nameof(Links), nameof(LibraryLink.Collection))]
public override IEnumerable<Collection> Collections
{
get => Links?.Where(x => x.Collection != null).Select(x => x.Collection);
set => Links = Utility.MergeLists(
value?.Select(x => new LibraryLink(this, x)),
Links?.Where(x => x.Collection == null))?.ToList();
}
public LibraryDE() {}
public LibraryDE(Library item)
{
Utility.Assign(this, item);
}
}
}

View File

@ -1,40 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
public class ShowDE : Show
{
[EditableRelation] [JsonReadOnly] [NotMergable] public virtual ICollection<GenreLink> GenreLinks { get; set; }
[ExpressionRewrite(nameof(GenreLinks), nameof(GenreLink.Child))]
public override IEnumerable<Genre> Genres
{
get => GenreLinks?.Select(x => x.Child);
set => GenreLinks = value?.Select(x => new GenreLink(this, x)).ToList();
}
[JsonReadOnly] [NotMergable] public virtual ICollection<LibraryLink> LibraryLinks { get; set; }
[ExpressionRewrite(nameof(LibraryLinks), nameof(LibraryLink.Library))]
public override IEnumerable<Library> Libraries
{
get => LibraryLinks?.Select(x => x.Library);
set => LibraryLinks = value?.Select(x => new LibraryLink(x, this)).ToList();
}
[JsonReadOnly] [NotMergable] public virtual ICollection<CollectionLink> CollectionLinks { get; set; }
[ExpressionRewrite(nameof(CollectionLinks), nameof(CollectionLink.Parent))]
public override IEnumerable<Collection> Collections
{
get => CollectionLinks?.Select(x => x.Parent);
set => CollectionLinks = value?.Select(x => new CollectionLink(x, this)).ToList();
}
public ShowDE() {}
public ShowDE(Show show)
{
Utility.Assign(this, show);
}
}
}

View File

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using IdentityServer4.Extensions;
using IdentityServer4.Services; using IdentityServer4.Services;
using Kyoo.Api; using Kyoo.Api;
using Kyoo.Controllers; using Kyoo.Controllers;
@ -36,19 +37,28 @@ namespace Kyoo
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
string publicUrl = _configuration.GetValue<string>("public_url");
services.AddSpaStaticFiles(configuration => services.AddSpaStaticFiles(configuration =>
{ {
configuration.RootPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "wwwroot"); configuration.RootPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
}); });
services.AddResponseCompression(x =>
{
x.EnableForHttps = true;
});
services.AddControllers() services.AddControllers()
.AddNewtonsoftJson(x => x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer()); .AddNewtonsoftJson(x =>
{
x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl);
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
});
services.AddHttpClient(); services.AddHttpClient();
services.AddDbContext<DatabaseContext>(options => services.AddDbContext<DatabaseContext>(options =>
{ {
options.UseLazyLoadingProxies() options.UseNpgsql(_configuration.GetConnectionString("Database"));
.UseNpgsql(_configuration.GetConnectionString("Database"));
// .EnableSensitiveDataLogging() // .EnableSensitiveDataLogging()
// .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
}, ServiceLifetime.Transient); }, ServiceLifetime.Transient);
@ -59,7 +69,6 @@ namespace Kyoo
}); });
string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
string publicUrl = _configuration.GetValue<string>("public_url");
services.AddIdentityCore<User>(o => services.AddIdentityCore<User>(o =>
{ {
@ -72,7 +81,6 @@ namespace Kyoo
services.AddIdentityServer(options => services.AddIdentityServer(options =>
{ {
options.IssuerUri = publicUrl; options.IssuerUri = publicUrl;
options.PublicOrigin = publicUrl;
options.UserInteraction.LoginUrl = publicUrl + "login"; options.UserInteraction.LoginUrl = publicUrl + "login";
options.UserInteraction.ErrorUrl = publicUrl + "error"; options.UserInteraction.ErrorUrl = publicUrl + "error";
options.UserInteraction.LogoutUrl = publicUrl + "logout"; options.UserInteraction.LogoutUrl = publicUrl + "logout";
@ -92,6 +100,7 @@ namespace Kyoo
options.EnableTokenCleanup = true; options.EnableTokenCleanup = true;
}) })
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
.AddInMemoryApiScopes(IdentityContext.GetScopes())
.AddInMemoryApiResources(IdentityContext.GetApis()) .AddInMemoryApiResources(IdentityContext.GetApis())
.AddProfileService<AccountController>() .AddProfileService<AccountController>()
.AddSigninKeys(_configuration); .AddSigninKeys(_configuration);
@ -146,8 +155,10 @@ namespace Kyoo
services.AddScoped<IStudioRepository, StudioRepository>(); services.AddScoped<IStudioRepository, StudioRepository>();
services.AddScoped<IGenreRepository, GenreRepository>(); services.AddScoped<IGenreRepository, GenreRepository>();
services.AddScoped<IProviderRepository, ProviderRepository>(); services.AddScoped<IProviderRepository, ProviderRepository>();
services.AddScoped<DbContext, DatabaseContext>();
services.AddScoped<ILibraryManager, LibraryManager>(); services.AddScoped<ILibraryManager, LibraryManager>();
services.AddSingleton<IFileManager, FileManager>();
services.AddSingleton<ITranscoder, Transcoder>(); services.AddSingleton<ITranscoder, Transcoder>();
services.AddSingleton<IThumbnailsManager, ThumbnailsManager>(); services.AddSingleton<IThumbnailsManager, ThumbnailsManager>();
services.AddSingleton<IProviderManager, ProviderManager>(); services.AddSingleton<IProviderManager, ProviderManager>();
@ -157,7 +168,7 @@ namespace Kyoo
services.AddHostedService(provider => (TaskManager)provider.GetService<ITaskManager>()); services.AddHostedService(provider => (TaskManager)provider.GetService<ITaskManager>());
} }
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
@ -181,11 +192,29 @@ namespace Kyoo
app.UseRouting(); app.UseRouting();
app.Use((ctx, next) =>
{
ctx.Response.Headers.Remove("X-Powered-By");
ctx.Response.Headers.Remove("Server");
ctx.Response.Headers.Add("Feature-Policy", "autoplay 'self'; fullscreen");
ctx.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'");
ctx.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
ctx.Response.Headers.Add("Referrer-Policy", "no-referrer");
ctx.Response.Headers.Add("Access-Control-Allow-Origin", "null");
ctx.Response.Headers.Add("X-Content-Type-Options", "nosniff");
return next();
});
app.UseResponseCompression();
app.UseCookiePolicy(new CookiePolicyOptions app.UseCookiePolicy(new CookiePolicyOptions
{ {
MinimumSameSitePolicy = SameSiteMode.Strict MinimumSameSitePolicy = SameSiteMode.Strict
}); });
app.UseAuthentication(); app.UseAuthentication();
app.Use((ctx, next) =>
{
ctx.SetIdentityServerOrigin(_configuration.GetValue<string>("public_url"));
return next();
});
app.UseIdentityServer(); app.UseIdentityServer();
app.UseAuthorization(); app.UseAuthorization();

View File

@ -11,7 +11,7 @@ namespace Kyoo.Tasks
new PluginLoader(), new PluginLoader(),
new Crawler(), new Crawler(),
new MetadataProviderLoader(), new MetadataProviderLoader(),
new ReScan(), // new ReScan(),
new ExtractMetadata() new ExtractMetadata()
}; };
} }

View File

@ -75,9 +75,12 @@ namespace Kyoo.Controllers
ICollection<Library> libraries = argument == null ICollection<Library> libraries = argument == null
? await libraryManager.GetLibraries() ? await libraryManager.GetLibraries()
: new [] { await libraryManager.GetLibrary(argument)}; : new [] { await libraryManager.GetLibrary(argument)};
// TODO replace this grotesque way to load the providers.
if (argument != null && libraries.First() == null)
throw new ArgumentException($"No library found with the name {argument}");
foreach (Library library in libraries) foreach (Library library in libraries)
library.Providers = library.Providers; await libraryManager.Load(library, x => x.Providers);
foreach (Library library in libraries) foreach (Library library in libraries)
await Scan(library, episodes, tracks, cancellationToken); await Scan(library, episodes, tracks, cancellationToken);
@ -123,6 +126,7 @@ namespace Kyoo.Controllers
.GroupBy(Path.GetDirectoryName) .GroupBy(Path.GetDirectoryName)
.ToList(); .ToList();
// TODO If the library's path end with a /, the regex is broken.
IEnumerable<string> tasks = shows.Select(x => x.First()); IEnumerable<string> tasks = shows.Select(x => x.First());
foreach (string[] showTasks in tasks.BatchBy(_parallelTasks)) foreach (string[] showTasks in tasks.BatchBy(_parallelTasks))
await Task.WhenAll(showTasks await Task.WhenAll(showTasks
@ -287,7 +291,6 @@ namespace Kyoo.Controllers
show.Slug += $"-{show.StartYear}"; show.Slug += $"-{show.StartYear}";
await libraryManager.RegisterShow(show); await libraryManager.RegisterShow(show);
} }
await _thumbnailsManager.Validate(show.People);
await _thumbnailsManager.Validate(show); await _thumbnailsManager.Validate(show);
return show; return show;
} }
@ -347,16 +350,17 @@ namespace Kyoo.Controllers
Title = show.Title, Title = show.Title,
Path = episodePath, Path = episodePath,
Show = show, Show = show,
ShowID = show.ID ShowID = show.ID,
ShowSlug = show.Slug
}; };
episode.Tracks = await GetTracks(episode); episode.Tracks = await GetTracks(episode);
return episode; return episode;
} }
private async Task<IEnumerable<Track>> GetTracks(Episode episode) private async Task<ICollection<Track>> GetTracks(Episode episode)
{ {
episode.Tracks = (await _transcoder.ExtractInfos(episode.Path)) episode.Tracks = (await _transcoder.ExtractInfos(episode, false))
.Where(x => x.Type != StreamType.Font) .Where(x => x.Type != StreamType.Attachment)
.ToArray(); .ToArray();
return episode.Tracks; return episode.Tracks;
} }

View File

@ -28,9 +28,9 @@ namespace Kyoo.Tasks
IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService<IdentityDatabase>(); IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService<IdentityDatabase>();
ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>(); ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>();
databaseContext.Database.Migrate(); databaseContext!.Database.Migrate();
identityDatabase.Database.Migrate(); identityDatabase!.Database.Migrate();
identityContext.Database.Migrate(); identityContext!.Database.Migrate();
if (!identityContext.Clients.Any()) if (!identityContext.Clients.Any())
{ {

View File

@ -72,6 +72,7 @@ namespace Kyoo.Tasks
{ {
if (thumbs) if (thumbs)
await _thumbnails!.Validate(show, true); await _thumbnails!.Validate(show, true);
await _library.Load(show, x => x.Seasons);
foreach (Season season in show.Seasons) foreach (Season season in show.Seasons)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@ -84,6 +85,7 @@ namespace Kyoo.Tasks
{ {
if (thumbs) if (thumbs)
await _thumbnails!.Validate(season, true); await _thumbnails!.Validate(season, true);
await _library.Load(season, x => x.Episodes);
foreach (Episode episode in season.Episodes) foreach (Episode episode in season.Episodes)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@ -98,10 +100,11 @@ namespace Kyoo.Tasks
await _thumbnails!.Validate(episode, true); await _thumbnails!.Validate(episode, true);
if (subs) if (subs)
{ {
// TODO this doesn't work. await _library.Load(episode, x => x.Tracks);
IEnumerable<Track> tracks = (await _transcoder!.ExtractInfos(episode.Path)) episode.Tracks = (await _transcoder!.ExtractInfos(episode, true))
.Where(x => x.Type != StreamType.Font); .Where(x => x.Type != StreamType.Attachment)
episode.Tracks = tracks; .Concat(episode.Tracks.Where(x => x.IsExternal))
.ToList();
await _library.EditEpisode(episode, false); await _library.EditEpisode(episode, false);
} }
} }

View File

@ -21,13 +21,15 @@ namespace Kyoo.Tasks
{ {
using IServiceScope serviceScope = serviceProvider.CreateScope(); using IServiceScope serviceScope = serviceProvider.CreateScope();
IProviderRepository providers = serviceScope.ServiceProvider.GetService<IProviderRepository>(); IProviderRepository providers = serviceScope.ServiceProvider.GetService<IProviderRepository>();
IThumbnailsManager thumbnails = serviceScope.ServiceProvider.GetService<IThumbnailsManager>();
IPluginManager pluginManager = serviceScope.ServiceProvider.GetService<IPluginManager>(); IPluginManager pluginManager = serviceScope.ServiceProvider.GetService<IPluginManager>();
foreach (IMetadataProvider provider in pluginManager.GetPlugins<IMetadataProvider>()) foreach (IMetadataProvider provider in pluginManager!.GetPlugins<IMetadataProvider>())
{ {
if (string.IsNullOrEmpty(provider.Provider.Slug)) if (string.IsNullOrEmpty(provider.Provider.Slug))
throw new ArgumentException($"Empty provider slug (name: {provider.Provider.Name})."); throw new ArgumentException($"Empty provider slug (name: {provider.Provider.Name}).");
await providers.CreateIfNotExists(provider.Provider); await providers!.CreateIfNotExists(provider.Provider);
await thumbnails!.Validate(provider.Provider);
} }
} }

View File

@ -1,127 +1,127 @@
using System; // using System;
using System.Collections.Generic; // using System.Collections.Generic;
using System.Linq; // using System.Linq;
using System.Threading; // using System.Threading;
using System.Threading.Tasks; // using System.Threading.Tasks;
using Kyoo.Controllers; // using Kyoo.Controllers;
using Kyoo.Models; // using Kyoo.Models;
using Microsoft.Extensions.DependencyInjection; // using Microsoft.Extensions.DependencyInjection;
//
namespace Kyoo.Tasks // namespace Kyoo.Tasks
{ // {
public class ReScan: ITask // public class ReScan: ITask
{ // {
public string Slug => "re-scan"; // public string Slug => "re-scan";
public string Name => "ReScan"; // public string Name => "ReScan";
public string Description => "Re download metadata of an item using it's external ids."; // public string Description => "Re download metadata of an item using it's external ids.";
public string HelpMessage => null; // public string HelpMessage => null;
public bool RunOnStartup => false; // public bool RunOnStartup => false;
public int Priority => 0; // public int Priority => 0;
//
//
private IServiceProvider _serviceProvider; // private IServiceProvider _serviceProvider;
private IThumbnailsManager _thumbnailsManager; // private IThumbnailsManager _thumbnailsManager;
private IProviderManager _providerManager; // private IProviderManager _providerManager;
private DatabaseContext _database; // private DatabaseContext _database;
//
public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) // public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null)
{ // {
using IServiceScope serviceScope = serviceProvider.CreateScope(); // using IServiceScope serviceScope = serviceProvider.CreateScope();
_serviceProvider = serviceProvider; // _serviceProvider = serviceProvider;
_thumbnailsManager = serviceProvider.GetService<IThumbnailsManager>(); // _thumbnailsManager = serviceProvider.GetService<IThumbnailsManager>();
_providerManager = serviceProvider.GetService<IProviderManager>(); // _providerManager = serviceProvider.GetService<IProviderManager>();
_database = serviceScope.ServiceProvider.GetService<DatabaseContext>(); // _database = serviceScope.ServiceProvider.GetService<DatabaseContext>();
//
if (arguments == null || !arguments.Contains('/')) // if (arguments == null || !arguments.Contains('/'))
return; // return;
//
string slug = arguments.Substring(arguments.IndexOf('/') + 1); // string slug = arguments.Substring(arguments.IndexOf('/') + 1);
switch (arguments.Substring(0, arguments.IndexOf('/'))) // switch (arguments.Substring(0, arguments.IndexOf('/')))
{ // {
case "show": // case "show":
await ReScanShow(slug); // await ReScanShow(slug);
break; // break;
case "season": // case "season":
await ReScanSeason(slug); // await ReScanSeason(slug);
break; // break;
} // }
} // }
//
private async Task ReScanShow(string slug) // private async Task ReScanShow(string slug)
{ // {
Show old; // Show old;
//
using (IServiceScope serviceScope = _serviceProvider.CreateScope()) // using (IServiceScope serviceScope = _serviceProvider.CreateScope())
{ // {
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); // ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
old = _database.Shows.FirstOrDefault(x => x.Slug == slug); // old = _database.Shows.FirstOrDefault(x => x.Slug == slug);
if (old == null) // if (old == null)
return; // return;
Library library = _database.LibraryLinks.First(x => x.Show == old && x.Library != null).Library; // Library library = _database.LibraryLinks.First(x => x.Show == old && x.Library != null).Library;
Show edited = await _providerManager.CompleteShow(old, library); // Show edited = await _providerManager.CompleteShow(old, library);
edited.ID = old.ID; // edited.ID = old.ID;
edited.Slug = old.Slug; // edited.Slug = old.Slug;
edited.Path = old.Path; // edited.Path = old.Path;
await libraryManager.EditShow(edited, true); // await libraryManager.EditShow(edited, true);
await _thumbnailsManager.Validate(edited, true); // await _thumbnailsManager.Validate(edited, true);
} // }
if (old.Seasons != null) // if (old.Seasons != null)
await Task.WhenAll(old.Seasons.Select(x => ReScanSeason(old, x))); // await Task.WhenAll(old.Seasons.Select(x => ReScanSeason(old, x)));
IEnumerable<Episode> orphans = old.Episodes.Where(x => x.Season == null).ToList(); // IEnumerable<Episode> orphans = old.Episodes.Where(x => x.Season == null).ToList();
if (orphans.Any()) // if (orphans.Any())
await Task.WhenAll(orphans.Select(x => ReScanEpisode(old, x))); // await Task.WhenAll(orphans.Select(x => ReScanEpisode(old, x)));
} // }
//
private async Task ReScanSeason(string seasonSlug) // private async Task ReScanSeason(string seasonSlug)
{ // {
string[] infos = seasonSlug.Split('-'); // string[] infos = seasonSlug.Split('-');
if (infos.Length != 2 || int.TryParse(infos[1], out int seasonNumber)) // if (infos.Length != 2 || int.TryParse(infos[1], out int seasonNumber))
return; // return;
string slug = infos[0]; // string slug = infos[0];
Show show = _database.Shows.FirstOrDefault(x => x.Slug == slug); // Show show = _database.Shows.FirstOrDefault(x => x.Slug == slug);
if (show == null) // if (show == null)
return; // return;
Season old = _database.Seasons.FirstOrDefault(x => x.SeasonNumber == seasonNumber && x.Show.ID == show.ID); // Season old = _database.Seasons.FirstOrDefault(x => x.SeasonNumber == seasonNumber && x.Show.ID == show.ID);
if (old == null) // if (old == null)
return; // return;
await ReScanSeason(show, old); // await ReScanSeason(show, old);
} // }
//
private async Task ReScanSeason(Show show, Season old) // private async Task ReScanSeason(Show show, Season old)
{ // {
using (IServiceScope serviceScope = _serviceProvider.CreateScope()) // using (IServiceScope serviceScope = _serviceProvider.CreateScope())
{ // {
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); // ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library; // Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library;
Season edited = await _providerManager.GetSeason(show, old.SeasonNumber, library); // Season edited = await _providerManager.GetSeason(show, old.SeasonNumber, library);
edited.ID = old.ID; // edited.ID = old.ID;
await libraryManager.EditSeason(edited, true); // await libraryManager.EditSeason(edited, true);
await _thumbnailsManager.Validate(edited, true); // await _thumbnailsManager.Validate(edited, true);
} // }
if (old.Episodes != null) // if (old.Episodes != null)
await Task.WhenAll(old.Episodes.Select(x => ReScanEpisode(show, x))); // await Task.WhenAll(old.Episodes.Select(x => ReScanEpisode(show, x)));
} // }
//
private async Task ReScanEpisode(Show show, Episode old) // private async Task ReScanEpisode(Show show, Episode old)
{ // {
using IServiceScope serviceScope = _serviceProvider.CreateScope(); // using IServiceScope serviceScope = _serviceProvider.CreateScope();
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); // ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
//
Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library; // Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library;
Episode edited = await _providerManager.GetEpisode(show, old.Path, old.SeasonNumber, old.EpisodeNumber, old.AbsoluteNumber, library); // Episode edited = await _providerManager.GetEpisode(show, old.Path, old.SeasonNumber, old.EpisodeNumber, old.AbsoluteNumber, library);
edited.ID = old.ID; // edited.ID = old.ID;
await libraryManager.EditEpisode(edited, true); // await libraryManager.EditEpisode(edited, true);
await _thumbnailsManager.Validate(edited, true); // await _thumbnailsManager.Validate(edited, true);
} // }
//
public Task<IEnumerable<string>> GetPossibleParameters() // public Task<IEnumerable<string>> GetPossibleParameters()
{ // {
return Task.FromResult<IEnumerable<string>>(null); // return Task.FromResult<IEnumerable<string>>(null);
} // }
//
public int? Progress() // public int? Progress()
{ // {
return null; // return null;
} // }
} // }
} // }

View File

@ -33,10 +33,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(
@ -63,10 +59,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(
@ -93,10 +85,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetLibraries(
@ -123,10 +111,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetLibraries(

View File

@ -2,12 +2,10 @@
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -19,11 +17,18 @@ namespace Kyoo.Api
public class EpisodeApi : CrudApi<Episode> public class EpisodeApi : CrudApi<Episode>
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IThumbnailsManager _thumbnails;
private readonly IFileManager _files;
public EpisodeApi(ILibraryManager libraryManager, IConfiguration configuration) public EpisodeApi(ILibraryManager libraryManager,
IConfiguration configuration,
IFileManager files,
IThumbnailsManager thumbnails)
: base(libraryManager.EpisodeRepository, configuration) : base(libraryManager.EpisodeRepository, configuration)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_files = files;
_thumbnails = thumbnails;
} }
[HttpGet("{episodeID:int}/show")] [HttpGet("{episodeID:int}/show")]
@ -77,10 +82,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Track> resources = await _libraryManager.GetTracks( ICollection<Track> resources = await _libraryManager.GetTracks(
@ -109,10 +110,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Track> resources = await _libraryManager.GetTracks( ICollection<Track> resources = await _libraryManager.GetTracks(
@ -143,10 +140,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Track> resources = await _libraryManager.GetTracks(ApiHelper.ParseWhere<Track>(where, x => x.Episode.Show.Slug == showSlug ICollection<Track> resources = await _libraryManager.GetTracks(ApiHelper.ParseWhere<Track>(where, x => x.Episode.Show.Slug == showSlug
@ -165,19 +158,24 @@ namespace Kyoo.Api
} }
} }
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/thumb")] [HttpGet("{id:int}/thumb")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(string showSlug, int seasonNumber, int episodeNumber) public async Task<IActionResult> GetThumb(int id)
{ {
string path = (await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber))?.Path; Episode episode = await _libraryManager.GetEpisode(id);
if (path == null) if (episode == null)
return NotFound(); return NotFound();
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
}
string thumb = Path.ChangeExtension(path, "jpg"); [HttpGet("{slug}/thumb")]
[Authorize(Policy="Read")]
if (System.IO.File.Exists(thumb)) public async Task<IActionResult> GetThumb(string slug)
return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); {
Episode episode = await _libraryManager.GetEpisode(slug);
if (episode == null)
return NotFound(); return NotFound();
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
} }
} }
} }

View File

@ -34,10 +34,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(
@ -64,10 +60,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(

View File

@ -45,10 +45,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(
@ -75,10 +71,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(
@ -105,10 +97,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetCollections(
@ -135,10 +123,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetCollections(
@ -165,10 +149,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(id, ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(id,
@ -195,10 +175,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(slug, ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(slug,

View File

@ -15,6 +15,7 @@ namespace Kyoo.Api
[Route("api/item")] [Route("api/item")]
[Route("api/items")] [Route("api/items")]
[ApiController] [ApiController]
[ResourceView]
public class LibraryItemApi : ControllerBase public class LibraryItemApi : ControllerBase
{ {
private readonly ILibraryItemRepository _libraryItems; private readonly ILibraryItemRepository _libraryItems;
@ -34,10 +35,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<LibraryItem> resources = await _libraryItems.GetAll( ICollection<LibraryItem> resources = await _libraryItems.GetAll(

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
@ -17,34 +16,34 @@ namespace Kyoo.Api
public class PeopleApi : CrudApi<People> public class PeopleApi : CrudApi<People>
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly string _peoplePath; private readonly IFileManager _files;
private readonly IThumbnailsManager _thumbs;
public PeopleApi(ILibraryManager libraryManager, IConfiguration configuration) public PeopleApi(ILibraryManager libraryManager,
IConfiguration configuration,
IFileManager files,
IThumbnailsManager thumbs)
: base(libraryManager.PeopleRepository, configuration) : base(libraryManager.PeopleRepository, configuration)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_peoplePath = configuration.GetValue<string>("peoplePath"); _files = files;
_thumbs = thumbs;
} }
[HttpGet("{id:int}/role")] [HttpGet("{id:int}/role")]
[HttpGet("{id:int}/roles")] [HttpGet("{id:int}/roles")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
[JsonDetailed] public async Task<ActionResult<Page<PeopleRole>>> GetRoles(int id,
public async Task<ActionResult<Page<ShowRole>>> GetRoles(int id,
[FromQuery] string sortBy, [FromQuery] string sortBy,
[FromQuery] int afterID, [FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<ShowRole> resources = await _libraryManager.GetRolesFromPeople(id, ICollection<PeopleRole> resources = await _libraryManager.GetRolesFromPeople(id,
ApiHelper.ParseWhere<ShowRole>(where), ApiHelper.ParseWhere<PeopleRole>(where),
new Sort<ShowRole>(sortBy), new Sort<PeopleRole>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
return Page(resources, limit); return Page(resources, limit);
@ -62,22 +61,17 @@ namespace Kyoo.Api
[HttpGet("{slug}/role")] [HttpGet("{slug}/role")]
[HttpGet("{slug}/roles")] [HttpGet("{slug}/roles")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
[JsonDetailed] public async Task<ActionResult<Page<PeopleRole>>> GetRoles(string slug,
public async Task<ActionResult<Page<ShowRole>>> GetRoles(string slug,
[FromQuery] string sortBy, [FromQuery] string sortBy,
[FromQuery] int afterID, [FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<ShowRole> resources = await _libraryManager.GetRolesFromPeople(slug, ICollection<PeopleRole> resources = await _libraryManager.GetRolesFromPeople(slug,
ApiHelper.ParseWhere<ShowRole>(where), ApiHelper.ParseWhere<PeopleRole>(where),
new Sort<ShowRole>(sortBy), new Sort<PeopleRole>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
return Page(resources, limit); return Page(resources, limit);
@ -92,15 +86,20 @@ namespace Kyoo.Api
} }
} }
[HttpGet("{id:int}/poster")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetPeopleIcon(int id)
{
People people = await _libraryManager.GetPeople(id);
return _files.FileResult(await _thumbs.GetPeoplePoster(people));
}
[HttpGet("{slug}/poster")] [HttpGet("{slug}/poster")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public IActionResult GetPeopleIcon(string slug) public async Task<IActionResult> GetPeopleIcon(string slug)
{ {
string thumbPath = Path.Combine(_peoplePath, slug + ".jpg"); People people = await _libraryManager.GetPeople(slug);
if (!System.IO.File.Exists(thumbPath)) return _files.FileResult(await _thumbs.GetPeoplePoster(people));
return NotFound();
return new PhysicalFileResult(Path.GetFullPath(thumbPath), "image/jpg");
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
@ -13,12 +13,35 @@ namespace Kyoo.Api
[ApiController] [ApiController]
public class ProviderAPI : CrudApi<ProviderID> public class ProviderAPI : CrudApi<ProviderID>
{ {
private readonly IThumbnailsManager _thumbnails;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IFileManager _files;
public ProviderAPI(ILibraryManager libraryManager, IConfiguration config) public ProviderAPI(ILibraryManager libraryManager,
IConfiguration config,
IFileManager files,
IThumbnailsManager thumbnails)
: base(libraryManager.ProviderRepository, config) : base(libraryManager.ProviderRepository, config)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_files = files;
_thumbnails = thumbnails;
}
[HttpGet("{id:int}/logo")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(int id)
{
ProviderID provider = await _libraryManager.GetProvider(id);
return _files.FileResult(await _thumbnails.GetProviderLogo(provider));
}
[HttpGet("{slug}/logo")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(string slug)
{
ProviderID provider = await _libraryManager.GetProvider(slug);
return _files.FileResult(await _thumbnails.GetProviderLogo(provider));
} }
} }
} }

View File

@ -17,11 +17,18 @@ namespace Kyoo.Api
public class SeasonApi : CrudApi<Season> public class SeasonApi : CrudApi<Season>
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IThumbnailsManager _thumbs;
private readonly IFileManager _files;
public SeasonApi(ILibraryManager libraryManager, IConfiguration configuration) public SeasonApi(ILibraryManager libraryManager,
IConfiguration configuration,
IThumbnailsManager thumbs,
IFileManager files)
: base(libraryManager.SeasonRepository, configuration) : base(libraryManager.SeasonRepository, configuration)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_thumbs = thumbs;
_files = files;
} }
[HttpGet("{seasonID:int}/episode")] [HttpGet("{seasonID:int}/episode")]
@ -33,10 +40,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetEpisodes(
@ -64,10 +67,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetEpisodes(
@ -96,10 +95,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetEpisodes(
@ -137,5 +132,23 @@ namespace Kyoo.Api
{ {
return await _libraryManager.GetShow(showID); return await _libraryManager.GetShow(showID);
} }
[HttpGet("{id:int}/thumb")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(int id)
{
Season season = await _libraryManager.GetSeason(id);
await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetSeasonPoster(season));
}
[HttpGet("{slug}/thumb")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(string slug)
{
Season season = await _libraryManager.GetSeason(slug);
await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetSeasonPoster(season));
}
} }
} }

View File

@ -9,7 +9,6 @@ using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Kyoo.Api namespace Kyoo.Api
@ -20,12 +19,18 @@ namespace Kyoo.Api
public class ShowApi : CrudApi<Show> public class ShowApi : CrudApi<Show>
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private FileExtensionContentTypeProvider _provider; private readonly IFileManager _files;
private readonly IThumbnailsManager _thumbs;
public ShowApi(ILibraryManager libraryManager, IConfiguration configuration) public ShowApi(ILibraryManager libraryManager,
IFileManager files,
IThumbnailsManager thumbs,
IConfiguration configuration)
: base(libraryManager.ShowRepository, configuration) : base(libraryManager.ShowRepository, configuration)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_files = files;
_thumbs = thumbs;
} }
[HttpGet("{showID:int}/season")] [HttpGet("{showID:int}/season")]
@ -37,10 +42,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Season> resources = await _libraryManager.GetSeasons( ICollection<Season> resources = await _libraryManager.GetSeasons(
@ -67,10 +68,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Season> resources = await _libraryManager.GetSeasons( ICollection<Season> resources = await _libraryManager.GetSeasons(
@ -97,10 +94,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetEpisodes(
@ -127,10 +120,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetEpisodes(
@ -156,10 +145,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<PeopleRole> resources = await _libraryManager.GetPeopleFromShow(showID, ICollection<PeopleRole> resources = await _libraryManager.GetPeopleFromShow(showID,
@ -185,10 +170,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<PeopleRole> resources = await _libraryManager.GetPeopleFromShow(slug, ICollection<PeopleRole> resources = await _libraryManager.GetPeopleFromShow(slug,
@ -215,10 +196,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Genre> resources = await _libraryManager.GetGenres( ICollection<Genre> resources = await _libraryManager.GetGenres(
@ -245,10 +222,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Genre> resources = await _libraryManager.GetGenres( ICollection<Genre> resources = await _libraryManager.GetGenres(
@ -303,10 +276,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetLibraries(
@ -333,10 +302,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetLibraries(
@ -363,10 +328,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetCollections(
@ -393,10 +354,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 30) [FromQuery] int limit = 30)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetCollections(
@ -419,13 +376,11 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Dictionary<string, string>>> GetFonts(string slug) public async Task<ActionResult<Dictionary<string, string>>> GetFonts(string slug)
{ {
string path = (await _libraryManager.GetShow(slug))?.Path; Show show = await _libraryManager.GetShow(slug);
if (path == null) if (show == null)
return NotFound(); return NotFound();
path = Path.Combine(path, "Subtitles", "fonts"); string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments");
if (!Directory.Exists(path)) return (await _files.ListFiles(path))
return new Dictionary<string, string>();
return Directory.GetFiles(path)
.ToDictionary(Path.GetFileNameWithoutExtension, .ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}"); x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}");
} }
@ -433,64 +388,43 @@ namespace Kyoo.Api
[HttpGet("{showSlug}/font/{slug}")] [HttpGet("{showSlug}/font/{slug}")]
[HttpGet("{showSlug}/fonts/{slug}")] [HttpGet("{showSlug}/fonts/{slug}")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult> GetFont(string showSlug, string slug) public async Task<IActionResult> GetFont(string showSlug, string slug)
{ {
string path = (await _libraryManager.GetShow(showSlug))?.Path; Show show = await _libraryManager.GetShow(showSlug);
if (path == null) if (show == null)
return NotFound(); return NotFound();
string fontPath = Path.Combine(path, "Subtitles", "fonts", slug); string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug);
if (!System.IO.File.Exists(fontPath)) return _files.FileResult(path);
return NotFound();
if (_provider == null)
_provider = new FileExtensionContentTypeProvider();
_provider.TryGetContentType(path, out string contentType);
return PhysicalFile(fontPath, contentType ?? "application/x-font-ttf");
} }
[HttpGet("{slug}/poster")] [HttpGet("{slug}/poster")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult> GetPoster(string slug) public async Task<IActionResult> GetPoster(string slug)
{ {
string path = (await _libraryManager.GetShow(slug))?.Path; Show show = await _libraryManager.GetShow(slug);
if (path == null) if (show == null)
return NotFound();
string poster = Path.Combine(path, "poster.jpg");
if (System.IO.File.Exists(poster))
return new PhysicalFileResult(Path.GetFullPath(poster), "image/jpg");
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetShowPoster(show));
} }
[HttpGet("{slug}/logo")] [HttpGet("{slug}/logo")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(string slug) public async Task<IActionResult> GetLogo(string slug)
{ {
string path = (await _libraryManager.GetShow(slug))?.Path; Show show = await _libraryManager.GetShow(slug);
if (path == null) if (show == null)
return NotFound();
string logo = Path.Combine(path, "logo.png");
if (System.IO.File.Exists(logo))
return new PhysicalFileResult(Path.GetFullPath(logo), "image/png");
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetShowLogo(show));
} }
[HttpGet("{slug}/backdrop")] [HttpGet("{slug}/backdrop")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetBackdrop(string slug) public async Task<IActionResult> GetBackdrop(string slug)
{ {
string path = (await _libraryManager.GetShow(slug))?.Path; Show show = await _libraryManager.GetShow(slug);
if (path == null) if (show == null)
return NotFound();
string thumb = Path.Combine(path, "backdrop.jpg");
if (System.IO.File.Exists(thumb))
return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg");
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetShowBackdrop(show));
} }
} }
} }

View File

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -34,10 +33,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(
@ -64,10 +59,6 @@ namespace Kyoo.Api
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 20)
{ {
where.Remove("sortBy");
where.Remove("limit");
where.Remove("afterID");
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetShows(

View File

@ -14,10 +14,12 @@ namespace Kyoo.Api
public class SubtitleApi : ControllerBase public class SubtitleApi : ControllerBase
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IFileManager _files;
public SubtitleApi(ILibraryManager libraryManager) public SubtitleApi(ILibraryManager libraryManager, IFileManager files)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_files = files;
} }
@ -35,13 +37,12 @@ namespace Kyoo.Api
return BadRequest(new {error = ex.Message}); return BadRequest(new {error = ex.Message});
} }
if (subtitle == null) if (subtitle == null || subtitle.Type != StreamType.Subtitle)
return NotFound(); return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt") if (subtitle.Codec == "subrip" && extension == "vtt")
return new ConvertSubripToVtt(subtitle.Path); return new ConvertSubripToVtt(subtitle.Path, _files);
string mime = subtitle.Codec == "ass" ? "text/x-ssa" : "application/x-subrip"; return _files.FileResult(subtitle.Path);
return PhysicalFile(subtitle.Path, mime);
} }
} }
@ -49,27 +50,29 @@ namespace Kyoo.Api
public class ConvertSubripToVtt : IActionResult public class ConvertSubripToVtt : IActionResult
{ {
private readonly string _path; private readonly string _path;
private readonly IFileManager _files;
public ConvertSubripToVtt(string subtitlePath) public ConvertSubripToVtt(string subtitlePath, IFileManager files)
{ {
_path = subtitlePath; _path = subtitlePath;
_files = files;
} }
public async Task ExecuteResultAsync(ActionContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
string line; List<string> lines = new();
List<string> lines = new List<string>();
context.HttpContext.Response.StatusCode = 200; context.HttpContext.Response.StatusCode = 200;
context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt"); context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt");
await using (StreamWriter writer = new StreamWriter(context.HttpContext.Response.Body)) await using (StreamWriter writer = new(context.HttpContext.Response.Body))
{ {
await writer.WriteLineAsync("WEBVTT"); await writer.WriteLineAsync("WEBVTT");
await writer.WriteLineAsync(""); await writer.WriteLineAsync("");
await writer.WriteLineAsync(""); await writer.WriteLineAsync("");
using StreamReader reader = new StreamReader(_path); using StreamReader reader = _files.GetReader(_path);
string line;
while ((line = await reader.ReadLineAsync()) != null) while ((line = await reader.ReadLineAsync()) != null)
{ {
if (line == "") if (line == "")

View File

@ -1,94 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Microsoft.AspNetCore.Authorization;
namespace Kyoo.Api
{
public class ThumbnailController : ControllerBase
{
private readonly ILibraryManager _libraryManager;
private readonly string _peoplePath;
public ThumbnailController(ILibraryManager libraryManager, IConfiguration config)
{
_libraryManager = libraryManager;
_peoplePath = config.GetValue<string>("peoplePath");
}
[HttpGet("poster/{showSlug}")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetShowThumb(string showSlug)
{
string path = (await _libraryManager.GetShow(showSlug))?.Path;
if (path == null)
return NotFound();
string thumb = Path.Combine(path, "poster.jpg");
if (System.IO.File.Exists(thumb))
return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg");
return NotFound();
}
[HttpGet("logo/{showSlug}")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetShowLogo(string showSlug)
{
string path = (await _libraryManager.GetShow(showSlug))?.Path;
if (path == null)
return NotFound();
string thumb = Path.Combine(path, "logo.png");
if (System.IO.File.Exists(thumb))
return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg");
return NotFound();
}
[HttpGet("backdrop/{showSlug}")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetShowBackdrop(string showSlug)
{
string path = (await _libraryManager.GetShow(showSlug))?.Path;
if (path == null)
return NotFound();
string thumb = Path.Combine(path, "backdrop.jpg");
if (System.IO.File.Exists(thumb))
return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg");
return NotFound();
}
[HttpGet("peopleimg/{peopleSlug}")]
[Authorize(Policy="Read")]
public IActionResult GetPeopleIcon(string peopleSlug)
{
string thumbPath = Path.Combine(_peoplePath, peopleSlug + ".jpg");
if (!System.IO.File.Exists(thumbPath))
return NotFound();
return new PhysicalFileResult(Path.GetFullPath(thumbPath), "image/jpg");
}
[HttpGet("thumb/{showSlug}-s{seasonNumber}e{episodeNumber}")]
[Authorize(Policy="Read")]
public async Task<IActionResult> GetEpisodeThumb(string showSlug, int seasonNumber, int episodeNumber)
{
string path = (await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber))?.Path;
if (path == null)
return NotFound();
string thumb = Path.ChangeExtension(path, "jpg");
if (System.IO.File.Exists(thumb))
return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg");
return NotFound();
}
}
}

View File

@ -0,0 +1,56 @@
using System.Linq;
using System.Threading.Tasks;
using Kyoo.CommonApi;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace Kyoo.Api
{
[Route("api/track")]
[Route("api/tracks")]
[ApiController]
public class TrackApi : CrudApi<Track>
{
private readonly ILibraryManager _libraryManager;
public TrackApi(ILibraryManager libraryManager, IConfiguration configuration)
: base(libraryManager.TrackRepository, configuration)
{
_libraryManager = libraryManager;
}
[HttpGet("{id:int}/episode")]
[Authorize(Policy = "Read")]
public async Task<ActionResult<Episode>> GetEpisode(int id)
{
try
{
return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.ID == id));
}
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpGet("{slug}/episode")]
[Authorize(Policy = "Read")]
public async Task<ActionResult<Episode>> GetEpisode(string slug)
{
try
{
// TODO This won't work with the local repository implementation.
// TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor
return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.Slug == slug));
}
catch (ItemNotFound)
{
return NotFound();
}
}
}
}

View File

@ -1,12 +1,11 @@
using Kyoo.Controllers; using System.IO;
using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Api namespace Kyoo.Api
{ {
@ -16,14 +15,18 @@ namespace Kyoo.Api
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ITranscoder _transcoder; private readonly ITranscoder _transcoder;
private readonly IFileManager _files;
private readonly string _transmuxPath; private readonly string _transmuxPath;
private readonly string _transcodePath; private readonly string _transcodePath;
private FileExtensionContentTypeProvider _provider;
public VideoApi(ILibraryManager libraryManager, ITranscoder transcoder, IConfiguration config) public VideoApi(ILibraryManager libraryManager,
ITranscoder transcoder,
IConfiguration config,
IFileManager files)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_transcoder = transcoder; _transcoder = transcoder;
_files = files;
_transmuxPath = config.GetValue<string>("transmuxTempPath"); _transmuxPath = config.GetValue<string>("transmuxTempPath");
_transcodePath = config.GetValue<string>("transcodeTempPath"); _transcodePath = config.GetValue<string>("transcodeTempPath");
} }
@ -37,19 +40,6 @@ namespace Kyoo.Api
ctx.HttpContext.Response.Headers.Add("Expires", "0"); ctx.HttpContext.Response.Headers.Add("Expires", "0");
} }
private string _GetContentType(string path)
{
if (_provider == null)
{
_provider = new FileExtensionContentTypeProvider();
_provider.Mappings[".mkv"] = "video/x-matroska";
}
if (_provider.TryGetContentType(path, out string contentType))
return contentType;
return "video/mp4";
}
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
[HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
@ -60,9 +50,9 @@ namespace Kyoo.Api
return BadRequest(new {error = "Season number or episode number can not be negative."}); return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode != null && System.IO.File.Exists(episode.Path)) if (episode == null)
return PhysicalFile(episode.Path, _GetContentType(episode.Path), true);
return NotFound(); return NotFound();
return _files.FileResult(episode.Path, true);
} }
[HttpGet("{movieSlug}")] [HttpGet("{movieSlug}")]
@ -72,9 +62,9 @@ namespace Kyoo.Api
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode != null && System.IO.File.Exists(episode.Path)) if (episode == null)
return PhysicalFile(episode.Path, _GetContentType(episode.Path), true);
return NotFound(); return NotFound();
return _files.FileResult(episode.Path, true);
} }
@ -86,12 +76,12 @@ namespace Kyoo.Api
return BadRequest(new {error = "Season number or episode number can not be negative."}); return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null || !System.IO.File.Exists(episode.Path)) if (episode == null)
return NotFound(); return NotFound();
string path = await _transcoder.Transmux(episode); string path = await _transcoder.Transmux(episode);
if (path == null) if (path == null)
return StatusCode(500); return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true); return _files.FileResult(path, true);
} }
[HttpGet("transmux/{movieSlug}/master.m3u8")] [HttpGet("transmux/{movieSlug}/master.m3u8")]
@ -100,12 +90,12 @@ namespace Kyoo.Api
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null || !System.IO.File.Exists(episode.Path)) if (episode == null)
return NotFound(); return NotFound();
string path = await _transcoder.Transmux(episode); string path = await _transcoder.Transmux(episode);
if (path == null) if (path == null)
return StatusCode(500); return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true); return _files.FileResult(path, true);
} }
[HttpGet("transcode/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")] [HttpGet("transcode/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")]
@ -116,12 +106,12 @@ namespace Kyoo.Api
return BadRequest(new {error = "Season number or episode number can not be negative."}); return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null || !System.IO.File.Exists(episode.Path)) if (episode == null)
return NotFound(); return NotFound();
string path = await _transcoder.Transcode(episode); string path = await _transcoder.Transcode(episode);
if (path == null) if (path == null)
return StatusCode(500); return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true); return _files.FileResult(path, true);
} }
[HttpGet("transcode/{movieSlug}/master.m3u8")] [HttpGet("transcode/{movieSlug}/master.m3u8")]
@ -130,12 +120,12 @@ namespace Kyoo.Api
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null || !System.IO.File.Exists(episode.Path)) if (episode == null)
return NotFound(); return NotFound();
string path = await _transcoder.Transcode(episode); string path = await _transcoder.Transcode(episode);
if (path == null) if (path == null)
return StatusCode(500); return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true); return _files.FileResult(path, true);
} }

@ -1 +1 @@
Subproject commit 57d382f7246287a611892359e444355e745d0795 Subproject commit ab52f039021928cab9f6ed8c17a0488ca198ef74

View File

@ -18,7 +18,7 @@
"ConnectionStrings": { "ConnectionStrings": {
"Database": "Server=127.0.0.1; Port=5432; Database=kyooDB; User Id=kyoo; Password=kyooPassword; Pooling=true; MaxPoolSize=95; Timeout=30;" "Database": "Server=127.0.0.1; Port=5432; Database=kyooDB; User Id=kyoo; Password=kyooPassword; Pooling=true; MaxPoolSize=95; Timeout=30;"
}, },
"parallelTasks": "40", "parallelTasks": "1",
"scheduledTasks": { "scheduledTasks": {
"scan": "24:00:00" "scan": "24:00:00"
@ -29,6 +29,7 @@
"transmuxTempPath": "cached/kyoo/transmux", "transmuxTempPath": "cached/kyoo/transmux",
"transcodeTempPath": "cached/kyoo/transcode", "transcodeTempPath": "cached/kyoo/transcode",
"peoplePath": "people", "peoplePath": "people",
"providerPath": "providers",
"profilePicturePath": "users/", "profilePicturePath": "users/",
"plugins": "plugins/", "plugins": "plugins/",
"defaultPermissions": "read,play,write,admin", "defaultPermissions": "read,play,write,admin",

Some files were not shown because too many files have changed in this diff Show More