Feat rework images, delete providers

This commit is contained in:
Zoe Roux 2023-08-05 00:22:27 +09:00
parent e075306363
commit 386c6bf268
No known key found for this signature in database
50 changed files with 3601 additions and 1992 deletions

View File

@ -85,11 +85,6 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
IGenreRepository GenreRepository { get; }
/// <summary>
/// The repository that handle providers.
/// </summary>
IProviderRepository ProviderRepository { get; }
/// <summary>
/// The repository that handle users.
/// </summary>

View File

@ -446,25 +446,6 @@ namespace Kyoo.Abstractions.Controllers
Pagination limit = default);
}
/// <summary>
/// A repository to handle providers.
/// </summary>
public interface IProviderRepository : IRepository<Provider>
{
/// <summary>
/// Get a list of external ids that match all filters
/// </summary>
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <typeparam name="T">The type of metadata to retrieve</typeparam>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default)
where T : class, IMetadata;
}
/// <summary>
/// A repository to handle users.
/// </summary>

View File

@ -35,22 +35,20 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="item">
/// The item to cache images.
/// </param>
/// <param name="alwaysDownload">
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
Task DownloadImages<T>(T item)
where T : IThumbnails;
/// <summary>
/// Retrieve the local path of an image of the given item.
/// </summary>
/// <param name="item">The item to retrieve the poster from.</param>
/// <param name="imageId">The ID of the image. See <see cref="Images"/> for values.</param>
/// <param name="image">The ID of the image.</param>
/// <param name="quality">The quality of the image</param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
string? GetImagePath<T>(T item, int imageId)
string GetImagePath<T>(T item, string image, ImageQuality quality)
where T : IThumbnails;
}
}

View File

@ -85,7 +85,13 @@ namespace Kyoo.Abstractions.Models
public DateTime? EndAir { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <summary>
/// The type of this item (ether a collection, a show or a movie).
@ -110,7 +116,9 @@ namespace Kyoo.Abstractions.Models
Status = show.Status;
StartAir = show.StartAir;
EndAir = show.EndAir;
Images = show.Images;
Poster = show.Poster;
Thumbnail = show.Thumbnail;
Logo = show.Logo;
Type = show.IsMovie ? ItemType.Movie : ItemType.Show;
}
@ -127,7 +135,9 @@ namespace Kyoo.Abstractions.Models
Status = Models.Status.Unknown;
StartAir = null;
EndAir = null;
Images = collection.Images;
Poster = collection.Poster;
Thumbnail = collection.Thumbnail;
Logo = collection.Logo;
Type = ItemType.Collection;
}
@ -143,7 +153,9 @@ namespace Kyoo.Abstractions.Models
Status = x.Status,
StartAir = x.StartAir,
EndAir = x.EndAir,
Images = x.Images,
Poster = x.Poster,
Thumbnail = x.Thumbnail,
Logo = x.Logo,
Type = x.IsMovie ? ItemType.Movie : ItemType.Show
};
@ -159,7 +171,9 @@ namespace Kyoo.Abstractions.Models
Status = Models.Status.Unknown,
StartAir = null,
EndAir = null,
Images = x.Images,
Poster = x.Poster,
Thumbnail = x.Thumbnail,
Logo = x.Logo,
Type = ItemType.Collection
};

View File

@ -14,11 +14,6 @@
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Linq.Expressions;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models
{
@ -27,29 +22,6 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public class MetadataID
{
/// <summary>
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
/// </summary>
public static Expression<Func<MetadataID, object>> PrimaryKey
{
get { return x => new { First = x.ResourceID, Second = x.ProviderID }; }
}
/// <summary>
/// The ID of the resource which possess the metadata.
/// </summary>
[SerializeIgnore] public int ResourceID { get; set; }
/// <summary>
/// The ID of the provider.
/// </summary>
[SerializeIgnore] public int ProviderID { get; set; }
/// <summary>
/// The provider that can do something with this ID.
/// </summary>
public Provider Provider { get; set; }
/// <summary>
/// The ID of the resource on the external provider.
/// </summary>

View File

@ -39,7 +39,13 @@ namespace Kyoo.Abstractions.Models
public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <summary>
/// The description of this collection.
@ -57,6 +63,6 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
public Dictionary<string, MetadataID> ExternalId { get; set; }
}
}

View File

@ -127,9 +127,6 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public string Path { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// The title of this episode.
/// </summary>
@ -146,7 +143,16 @@ namespace Kyoo.Abstractions.Models
public DateTime? ReleaseDate { get; set; }
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataID> ExternalId { get; set; }
/// <summary>
/// Get the slug of an episode.

View File

@ -16,11 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models
{
@ -32,67 +28,6 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The link to metadata providers that this show has. See <see cref="MetadataID"/> for more information.
/// </summary>
[EditableRelation]
[LoadableRelation]
public ICollection<MetadataID> ExternalIDs { get; set; }
}
/// <summary>
/// A static class containing extensions method for every <see cref="IMetadata"/> class.
/// This allow one to use metadata more easily.
/// </summary>
public static class MetadataExtension
{
/// <summary>
/// Retrieve the internal provider's ID of an item using it's provider slug.
/// </summary>
/// <remarks>
/// This method will never return anything if the <see cref="IMetadata.ExternalIDs"/> are not loaded.
/// </remarks>
/// <param name="self">An instance of <see cref="IMetadata"/> to retrieve the ID from.</param>
/// <param name="provider">The slug of the provider</param>
/// <returns>The <see cref="MetadataID.DataID"/> field of the asked provider.</returns>
[CanBeNull]
public static string GetID(this IMetadata self, string provider)
{
return self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID;
}
/// <summary>
/// Retrieve the internal provider's ID of an item using it's provider slug.
/// If the ID could be found, it is converted to the <typeparamref name="T"/> type and <c>true</c> is returned.
/// </summary>
/// <remarks>
/// This method will never succeed if the <see cref="IMetadata.ExternalIDs"/> are not loaded.
/// </remarks>
/// <param name="self">An instance of <see cref="IMetadata"/> to retrieve the ID from.</param>
/// <param name="provider">The slug of the provider</param>
/// <param name="id">
/// The <see cref="MetadataID.DataID"/> field of the asked provider parsed
/// and converted to the <typeparamref name="T"/> type.
/// It is only relevant if this method returns <c>true</c>.
/// </param>
/// <typeparam name="T">The type to convert the <see cref="MetadataID.DataID"/> to.</typeparam>
/// <returns><c>true</c> if this method succeeded, <c>false</c> otherwise.</returns>
public static bool TryGetID<T>(this IMetadata self, string provider, out T id)
{
string dataID = self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID;
if (dataID == null)
{
id = default;
return false;
}
try
{
id = (T)Convert.ChangeType(dataID, typeof(T));
}
catch
{
id = default;
return false;
}
return true;
}
public Dictionary<string, MetadataID> ExternalId { get; set; }
}
}

View File

@ -16,8 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using Kyoo.Abstractions.Controllers;
using System.ComponentModel.DataAnnotations;
namespace Kyoo.Abstractions.Models
{
@ -25,53 +24,56 @@ namespace Kyoo.Abstractions.Models
/// An interface representing items that contains images (like posters, thumbnails, logo, banners...)
/// </summary>
public interface IThumbnails
{
/// <summary>
/// The list of images mapped to a certain index.
/// </summary>
/// <remarks>
/// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/>
/// </remarks>
/// <example>{"0": "example.com/dune/poster"}</example>
public Dictionary<int, string> Images { get; set; }
}
/// <summary>
/// A class containing constant values for images. To be used as index of a <see cref="IThumbnails.Images"/>.
/// </summary>
public static class Images
{
/// <summary>
/// A poster is a 9/16 format image with the cover of the resource.
/// </summary>
public const int Poster = 0;
public Image Poster { get; set; }
/// <summary>
/// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually
/// is not an official image.
/// </summary>
public const int Thumbnail = 1;
public Image Thumbnail { get; set; }
/// <summary>
/// A logo is a small image representing the resource.
/// </summary>
public const int Logo = 2;
public Image Logo { get; set; }
}
public class Image
{
/// <summary>
/// The original image from another server.
/// </summary>
public string Source { get; set; }
/// <summary>
/// A video of a few minutes that tease the content.
/// A hash to display as placeholder while the image is loading.
/// </summary>
public const int Trailer = 3;
[MaxLength(32)]
public string Blurhash { get; set; }
}
/// <summary>
/// The quality of an image
/// </summary>
public enum ImageQuality
{
/// <summary>
/// Small
/// </summary>
Small,
/// <summary>
/// Retrieve the name of an image using it's ID. It is also used by the serializer to retrieve all named images.
/// If a plugin adds a new image type, it should add it's value and name here to allow the serializer to add it.
/// Medium
/// </summary>
public static Dictionary<int, string> ImageName { get; } = new()
{
[Poster] = nameof(Poster),
[Thumbnail] = nameof(Thumbnail),
[Logo] = nameof(Logo),
[Trailer] = nameof(Trailer)
};
Medium,
/// <summary>
/// Large
/// </summary>
Large,
}
}

View File

@ -42,11 +42,6 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public string[] Paths { get; set; }
/// <summary>
/// The list of <see cref="Provider"/> used for items in this library.
/// </summary>
[EditableRelation][LoadableRelation] public ICollection<Provider> Providers { get; set; }
/// <summary>
/// The list of shows in this library.
/// </summary>

View File

@ -38,10 +38,16 @@ namespace Kyoo.Abstractions.Models
public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataID> ExternalId { get; set; }
/// <summary>
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.

View File

@ -1,71 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// A dead class that will be removed later.
/// </summary>
// TODO: Delete this class
public class Provider : IResource, IThumbnails
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this provider.
/// </summary>
public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// The list of libraries that uses this provider.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
/// <summary>
/// Create a new, default, <see cref="Provider"/>
/// </summary>
public Provider() { }
/// <summary>
/// Create a new <see cref="Provider"/> and specify it's <see cref="Name"/>.
/// The <see cref="Slug"/> is automatically calculated from it's name.
/// </summary>
/// <param name="name">The name of this provider.</param>
/// <param name="logo">The logo of this provider.</param>
public Provider(string name, string logo)
{
Slug = Utility.ToSlug(name);
Name = name;
Images = new Dictionary<int, string>
{
[Models.Images.Logo] = logo
};
}
}
}

View File

@ -99,10 +99,16 @@ namespace Kyoo.Abstractions.Models
public DateTime? EndDate { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataID> ExternalId { get; set; }
/// <summary>
/// The list of episodes that this season contains.

View File

@ -59,13 +59,6 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public Status Status { get; set; }
/// <summary>
/// An URL to a trailer.
/// </summary>
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string TrailerUrl => Images?.GetValueOrDefault(Models.Images.Trailer);
/// <summary>
/// The date this show started airing. It can be null if this is unknown.
/// </summary>
@ -78,16 +71,27 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public DateTime? EndAir { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// True if this show represent a movie, false otherwise.
/// </summary>
public bool IsMovie { get; set; }
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <summary>
/// A video of a few minutes that tease the content.
/// </summary>
public string Trailer { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataID> ExternalId { get; set; }
/// <summary>
/// The ID of the Studio that made this show.

View File

@ -44,7 +44,7 @@ namespace Kyoo.Abstractions.Models
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
public Dictionary<string, MetadataID> ExternalId { get; set; }
/// <summary>
/// Create a new, empty, <see cref="Studio"/>.

View File

@ -24,7 +24,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// A single user of the app.
/// </summary>
public class User : IResource, IThumbnails
public class User : IResource
{
/// <inheritdoc />
public int ID { get; set; }
@ -59,8 +59,10 @@ namespace Kyoo.Abstractions.Models
[SerializeIgnore]
public Dictionary<string, string> ExtraData { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// A logo is a small image representing the resource.
/// </summary>
public Image Logo { get; set; }
/// <summary>
/// The list of shows the user has finished.

View File

@ -103,7 +103,13 @@ namespace Kyoo.Abstractions.Models
public bool IsMovie { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
public Image Poster { get; set; }
/// <inheritdoc />
public Image Thumbnail { get; set; }
/// <inheritdoc />
public Image Logo { get; set; }
/// <summary>
/// The transcoder's info for this item. This include subtitles, fonts, chapters...
@ -145,7 +151,9 @@ namespace Kyoo.Abstractions.Models
Title = ep.Title,
Overview = ep.Overview,
ReleaseDate = ep.ReleaseDate,
Images = ep.Show.Images,
Poster = ep.Poster,
Thumbnail = ep.Thumbnail,
Logo = ep.Logo,
PreviousEpisode = ep.Show.IsMovie
? null
: (await library.GetAll<Episode>(

View File

@ -66,9 +66,6 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc />
public IGenreRepository GenreRepository { get; }
/// <inheritdoc />
public IProviderRepository ProviderRepository { get; }
/// <inheritdoc />
public IUserRepository UserRepository { get; }
@ -89,7 +86,6 @@ namespace Kyoo.Core.Controllers
PeopleRepository = GetRepository<People>() as IPeopleRepository;
StudioRepository = GetRepository<Studio>() as IStudioRepository;
GenreRepository = GetRepository<Genre>() as IGenreRepository;
ProviderRepository = GetRepository<Provider>() as IProviderRepository;
UserRepository = GetRepository<User>() as IUserRepository;
}
@ -259,10 +255,6 @@ namespace Kyoo.Core.Controllers
return (obj, member: memberName) 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),
@ -272,11 +264,6 @@ namespace Kyoo.Core.Controllers
.Then(x => l.Collections = x),
(Collection c, nameof(Collection.ExternalIDs)) => _SetRelation(c,
ProviderRepository.GetMetadataID<Collection>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Collection c, nameof(Collection.Shows)) => ShowRepository
.GetAll(x => x.Collections.Any(y => y.ID == obj.ID))
.Then(x => c.Shows = x),
@ -286,11 +273,6 @@ namespace Kyoo.Core.Controllers
.Then(x => c.Libraries = x),
(Show s, nameof(Show.ExternalIDs)) => _SetRelation(s,
ProviderRepository.GetMetadataID<Show>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Show s, nameof(Show.Genres)) => GenreRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => s.Genres = x),
@ -326,11 +308,6 @@ namespace Kyoo.Core.Controllers
}),
(Season s, nameof(Season.ExternalIDs)) => _SetRelation(s,
ProviderRepository.GetMetadataID<Season>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Season s, nameof(Season.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Season.ID == obj.ID),
(x, y) => x.Episodes = y,
@ -345,11 +322,6 @@ namespace Kyoo.Core.Controllers
}),
(Episode e, nameof(Episode.ExternalIDs)) => _SetRelation(e,
ProviderRepository.GetMetadataID<Episode>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Episode e, nameof(Episode.Show)) => ShowRepository
.GetOrDefault(x => x.Episodes.Any(y => y.ID == obj.ID))
.Then(x =>
@ -376,27 +348,10 @@ namespace Kyoo.Core.Controllers
.GetAll(x => x.Studio.ID == obj.ID)
.Then(x => s.Shows = x),
(Studio s, nameof(Studio.ExternalIDs)) => _SetRelation(s,
ProviderRepository.GetMetadataID<Studio>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(People p, nameof(People.ExternalIDs)) => _SetRelation(p,
ProviderRepository.GetMetadataID<People>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(People p, nameof(People.Roles)) => PeopleRepository
.GetFromPeople(obj.ID)
.Then(x => p.Roles = x),
(Provider p, nameof(Provider.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 {memberName} of {obj.Slug}.")
};
}

View File

@ -37,11 +37,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override Sort<Collection> DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
@ -49,12 +44,10 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="CollectionRepository"/>.
/// </summary>
/// <param name="database">The database handle to use</param>
/// /// <param name="providers">A provider repository</param>
public CollectionRepository(DatabaseContext database, IProviderRepository providers)
public CollectionRepository(DatabaseContext database)
: base(database)
{
_database = database;
_providers = providers;
}
/// <inheritdoc />
@ -82,33 +75,8 @@ namespace Kyoo.Core.Controllers
{
await base.Validate(resource);
if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("The collection's slug must be set and not empty");
if (string.IsNullOrEmpty(resource.Name))
throw new ArgumentException("The collection's name must be set and not empty");
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Collection resource, Collection changed, bool resetOld)
{
await Validate(changed);
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
}
/// <inheritdoc />

View File

@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
private readonly IShowRepository _shows;
/// <inheritdoc />
@ -59,14 +54,11 @@ namespace Kyoo.Core.Controllers
/// </summary>
/// <param name="database">The database handle to use.</param>
/// <param name="shows">A show repository</param>
/// <param name="providers">A provider repository</param>
public EpisodeRepository(DatabaseContext database,
IShowRepository shows,
IProviderRepository providers)
IShowRepository shows)
: base(database)
{
_database = database;
_providers = providers;
_shows = shows;
// Edit episode slugs when the show's slug changes.
@ -160,18 +152,6 @@ namespace Kyoo.Core.Controllers
return obj;
}
/// <inheritdoc />
protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
{
await Validate(changed);
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
}
/// <inheritdoc />
protected override async Task Validate(Episode resource)
{
@ -185,17 +165,6 @@ namespace Kyoo.Core.Controllers
}
resource.ShowID = resource.Show.ID;
}
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Episode>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc />
@ -206,7 +175,6 @@ namespace Kyoo.Core.Controllers
int epCount = await _database.Episodes.Where(x => x.ShowID == obj.ShowID).Take(2).CountAsync();
_database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync();
await base.Delete(obj);
if (epCount == 1)

View File

@ -38,11 +38,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override Sort<Library> DefaultSort => new Sort<Library>.By(x => x.ID);
@ -50,12 +45,10 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="LibraryRepository"/> instance.
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">The provider repository</param>
public LibraryRepository(DatabaseContext database, IProviderRepository providers)
public LibraryRepository(DatabaseContext database)
: base(database)
{
_database = database;
_providers = providers;
}
/// <inheritdoc />
@ -90,29 +83,6 @@ namespace Kyoo.Core.Controllers
throw new ArgumentException("The library's name must be set and not empty");
if (resource.Paths == null || !resource.Paths.Any())
throw new ArgumentException("The library should have a least one path.");
if (resource.Providers != null)
{
resource.Providers = await resource.Providers
.SelectAsync(async x =>
_database.LocalEntity<Provider>(x.Slug)
?? await _providers.CreateIfNotExists(x)
)
.ToListAsync();
_database.AttachRange(resource.Providers);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{
await Validate(changed);
if (changed.Providers != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.Providers).LoadAsync();
resource.Providers = changed.Providers;
}
}
/// <inheritdoc />

View File

@ -341,6 +341,8 @@ namespace Kyoo.Core.Controllers
if (obj == null)
throw new ArgumentNullException(nameof(obj));
await Validate(obj);
// if (obj is IThumbnails thumbs)
// await _thumbnailsManager.DownloadImages(thumbs);
return obj;
}

View File

@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// A lazy loaded show repository to validate requests from shows.
/// </summary>
@ -56,15 +51,12 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="PeopleRepository"/>
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A lazy loaded show repository</param>
public PeopleRepository(DatabaseContext database,
IProviderRepository providers,
Lazy<IShowRepository> shows)
: base(database)
{
_database = database;
_providers = providers;
_shows = shows;
}
@ -94,17 +86,6 @@ namespace Kyoo.Core.Controllers
{
await base.Validate(resource);
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<People>().AttachRange(resource.ExternalIDs);
}
if (resource.Roles != null)
{
foreach (PeopleRole role in resource.Roles)
@ -127,12 +108,6 @@ namespace Kyoo.Core.Controllers
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;
}
}
/// <inheritdoc />
@ -142,7 +117,6 @@ namespace Kyoo.Core.Controllers
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
obj.Roles.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync();
await base.Delete(obj);

View File

@ -1,98 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository to handle providers.
/// </summary>
public class ProviderRepository : LocalRepository<Provider>, IProviderRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// Create a new <see cref="ProviderRepository" />.
/// </summary>
/// <param name="database">The database handle</param>
public ProviderRepository(DatabaseContext database)
: base(database)
{
_database = database;
}
/// <inheritdoc />
protected override Sort<Provider> DefaultSort => new Sort<Provider>.By(x => x.Slug);
/// <inheritdoc />
public override async Task<ICollection<Provider>> Search(string query)
{
return await Sort(
_database.Providers
.Where(_database.Like<Provider>(x => x.Name, $"%{query}%"))
)
.Take(20)
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Provider> Create(Provider obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.Slug));
OnResourceCreated(obj);
return obj;
}
/// <inheritdoc />
public override async Task Delete(Provider obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
await base.Delete(obj);
}
/// <inheritdoc />
public async Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default)
where T : class, IMetadata
{
return await _database.MetadataIds<T>()
.Include(y => y.Provider)
.Where(where)
.ToListAsync();
}
}
}

View File

@ -38,11 +38,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc/>
protected override Sort<Season> DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
@ -51,14 +46,11 @@ namespace Kyoo.Core.Controllers
/// </summary>
/// <param name="database">The database handle that will be used</param>
/// <param name="shows">A shows repository</param>
/// <param name="providers">A provider repository</param>
public SeasonRepository(DatabaseContext database,
IShowRepository shows,
IProviderRepository providers)
IShowRepository shows)
: base(database)
{
_database = database;
_providers = providers;
// Edit seasons slugs when the show's slug changes.
shows.OnEdited += (show) =>
@ -140,29 +132,6 @@ namespace Kyoo.Core.Controllers
}
resource.ShowID = resource.Show.ID;
}
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc/>
protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
{
await Validate(changed);
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
}
/// <inheritdoc/>

View File

@ -52,11 +52,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly IGenreRepository _genres;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override Sort<Show> DefaultSort => new Sort<Show>.By(x => x.Title);
@ -67,19 +62,16 @@ namespace Kyoo.Core.Controllers
/// <param name="studios">A studio repository</param>
/// <param name="people">A people repository</param>
/// <param name="genres">A genres repository</param>
/// <param name="providers">A provider repository</param>
public ShowRepository(DatabaseContext database,
IStudioRepository studios,
IPeopleRepository people,
IGenreRepository genres,
IProviderRepository providers)
IGenreRepository genres)
: base(database)
{
_database = database;
_studios = studios;
_people = people;
_genres = genres;
_providers = providers;
}
/// <inheritdoc />
@ -124,17 +116,6 @@ namespace Kyoo.Core.Controllers
_database.AttachRange(resource.Genres);
}
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Show>().AttachRange(resource.ExternalIDs);
}
if (resource.People != null)
{
foreach (PeopleRole role in resource.People)
@ -172,12 +153,6 @@ namespace Kyoo.Core.Controllers
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;
}
}
/// <inheritdoc />

View File

@ -38,11 +38,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override Sort<Studio> DefaultSort => new Sort<Studio>.By(x => x.Name);
@ -50,12 +45,10 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="StudioRepository"/>.
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">A provider repository</param>
public StudioRepository(DatabaseContext database, IProviderRepository providers)
public StudioRepository(DatabaseContext database)
: base(database)
{
_database = database;
_providers = providers;
}
/// <inheritdoc />
@ -83,30 +76,7 @@ namespace Kyoo.Core.Controllers
protected override async Task Validate(Studio resource)
{
resource.Slug ??= Utility.ToSlug(resource.Name);
await base.Validate(resource);
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Studio>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Studio resource, Studio 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);
}
/// <inheritdoc />

View File

@ -18,13 +18,12 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging;
using SkiaSharp;
#nullable enable
@ -54,18 +53,18 @@ namespace Kyoo.Core.Controllers
_logger = logger;
}
/// <summary>
/// An helper function to download an image.
/// </summary>
/// <param name="url">The distant url of the image</param>
/// <param name="localPath">The local path of the image</param>
/// <param name="what">What is currently downloaded (used for errors)</param>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _DownloadImage(string url, string localPath, string what)
private static async Task _WriteTo(SKBitmap bitmap, string path)
{
if (url == localPath)
return false;
SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 18);
await using Stream reader = data.AsStream();
await using Stream file = File.Create(path);
await reader.CopyToAsync(file);
}
private async Task _DownloadImage(string? url, string localPath, string what)
{
if (url == null)
return;
try
{
_logger.LogInformation("Downloading image {What}", what);
@ -73,86 +72,53 @@ namespace Kyoo.Core.Controllers
HttpClient client = _clientFactory.CreateClient();
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string mime = response.Content.Headers.ContentType?.MediaType!;
await using Stream reader = await response.Content.ReadAsStreamAsync();
SKBitmap bitmap = SKBitmap.Decode(reader);
string extension = new FileExtensionContentTypeProvider()
.Mappings.FirstOrDefault(x => x.Value == mime)
.Key;
await using Stream local = File.Create(localPath + extension);
await reader.CopyToAsync(local);
return true;
bitmap.Resize(new SKSizeI(bitmap.Width, bitmap.Height), SKFilterQuality.High);
await _WriteTo(bitmap, $"{localPath}.{ImageQuality.Large.ToString().ToLowerInvariant()}.jpg");
bitmap.Resize(new SKSizeI(bitmap.Width, bitmap.Height), SKFilterQuality.Medium);
await _WriteTo(bitmap, $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.jpg");
bitmap.Resize(new SKSizeI(bitmap.Width, bitmap.Height), SKFilterQuality.Low);
await _WriteTo(bitmap, $"{localPath}.{ImageQuality.Small.ToString().ToLowerInvariant()}.jpg");
}
catch (Exception ex)
{
_logger.LogError(ex, "{What} could not be downloaded", what);
return false;
}
}
/// <inheritdoc />
public async Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
public async Task DownloadImages<T>(T item)
where T : IThumbnails
{
if (item == null)
throw new ArgumentNullException(nameof(item));
if (item.Images == null)
return false;
string name = item is IResource res ? res.Slug : "???";
bool ret = false;
foreach ((int id, string image) in item.Images.Where(x => x.Value != null))
{
string localPath = _GetPrivateImagePath(item, id);
if (alwaysDownload || !Path.Exists(localPath))
ret |= await _DownloadImage(image, localPath, $"The image n {id} of {name}");
}
return ret;
await _DownloadImage(item.Poster.Source, _GetBaseImagePath(item, "poster"), $"The poster of {name}");
await _DownloadImage(item.Thumbnail.Source, _GetBaseImagePath(item, "thumbnail"), $"The poster of {name}");
await _DownloadImage(item.Logo.Source, _GetBaseImagePath(item, "logo"), $"The poster of {name}");
}
/// <summary>
/// Retrieve the local path of an image of the given item <b>without an extension</b>.
/// </summary>
/// <param name="item">The item to retrieve the poster from.</param>
/// <param name="imageId">The ID of the image. See <see cref="Images"/> for values.</param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns>The path of the image for the given resource, <b>even if it does not exists</b></returns>
private static string _GetPrivateImagePath<T>(T item, int imageId)
private static string _GetBaseImagePath<T>(T item, string image)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
string directory = item switch
{
IResource res => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant(), res.Slug),
_ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant())
};
Directory.CreateDirectory(directory);
string imageName = imageId switch
{
Images.Poster => "poster",
Images.Logo => "logo",
Images.Thumbnail => "thumbnail",
Images.Trailer => "trailer",
_ => $"{imageId}"
};
return Path.Combine(directory, imageName);
return Path.Combine(directory, image);
}
/// <inheritdoc />
public string? GetImagePath<T>(T item, int imageId)
public string GetImagePath<T>(T item, string image, ImageQuality quality)
where T : IThumbnails
{
string basePath = _GetPrivateImagePath(item, imageId);
string directory = Path.GetDirectoryName(basePath)!;
string baseFile = Path.GetFileName(basePath);
if (!Directory.Exists(directory))
return null;
return Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly)
.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == baseFile);
return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.jpg";
}
}
}

View File

@ -57,7 +57,6 @@ namespace Kyoo.Core
builder.RegisterRepository<IPeopleRepository, PeopleRepository>();
builder.RegisterRepository<IStudioRepository, StudioRepository>();
builder.RegisterRepository<IGenreRepository, GenreRepository>();
builder.RegisterRepository<IProviderRepository, ProviderRepository>();
builder.RegisterRepository<IUserRepository, UserRepository>();
}

View File

@ -6,9 +6,11 @@
<ItemGroup>
<PackageReference Include="AspNetCore.Proxy" Version="4.4.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
</ItemGroup>
<ItemGroup>

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.IO;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
@ -25,7 +24,6 @@ using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
@ -59,37 +57,7 @@ namespace Kyoo.Core.Api
_thumbs = thumbs;
}
/// <summary>
/// Get the content type of a file using it's extension.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
/// <returns>The content type of the file</returns>
private static string _GetContentType(string path)
{
FileExtensionContentTypeProvider provider = new();
if (provider.TryGetContentType(path, out string contentType))
return contentType;
throw new NotImplementedException($"Can't get the content type of the file at: {path}");
}
/// <summary>
/// Get image
/// </summary>
/// <remarks>
/// Get an image for the specified item.
/// List of commonly available images:<br/>
/// - Poster: Image 0, also available at /poster<br/>
/// - Thumbnail: Image 1, also available at /thumbnail<br/>
/// - Logo: Image 3, also available at /logo<br/>
/// <br/>
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="image">The number of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
private async Task<IActionResult> _GetImage(Identifier identifier, int image)
private async Task<IActionResult> _GetImage(Identifier identifier, string image, ImageQuality? quality)
{
T resource = await identifier.Match(
id => Repository.GetOrDefault(id),
@ -97,10 +65,10 @@ namespace Kyoo.Core.Api
);
if (resource == null)
return NotFound();
string path = _thumbs.GetImagePath(resource, image);
string path = _thumbs.GetImagePath(resource, image, quality ?? ImageQuality.Large);
if (path == null || !System.IO.File.Exists(path))
return NotFound();
return PhysicalFile(Path.GetFullPath(path), _GetContentType(path), true);
return PhysicalFile(Path.GetFullPath(path), "image/jpeg", true);
}
/// <summary>
@ -110,17 +78,18 @@ namespace Kyoo.Core.Api
/// Get the poster for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="quality">The quality of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/poster", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/poster")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetPoster(Identifier identifier)
public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
{
return _GetImage(identifier, Images.Poster);
return _GetImage(identifier, "poster", quality);
}
/// <summary>
@ -130,17 +99,18 @@ namespace Kyoo.Core.Api
/// Get the logo for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="quality">The quality of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/logo", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/logo")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetLogo(Identifier identifier)
public Task<IActionResult> GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
{
return _GetImage(identifier, Images.Logo);
return _GetImage(identifier, "logo", quality);
}
/// <summary>
@ -150,15 +120,16 @@ namespace Kyoo.Core.Api
/// Get the thumbnail for the specified item.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="quality">The quality of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
[HttpGet("{identifier:id}/thumbnail")]
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)]
public Task<IActionResult> GetBackdrop(Identifier identifier)
public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
{
return _GetImage(identifier, Images.Thumbnail);
return _GetImage(identifier, "thumbnail", quality);
}
/// <inheritdoc/>

View File

@ -79,28 +79,28 @@ namespace Kyoo.Core.Api
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
if (!type.IsAssignableTo(typeof(IThumbnails)))
if (!type.IsAssignableTo(typeof(Image)))
return properties;
foreach ((int id, string image) in Images.ImageName)
{
properties.Add(new JsonProperty
{
DeclaringType = type,
PropertyName = image.ToLower(),
UnderlyingName = image,
PropertyType = typeof(string),
Readable = true,
Writable = false,
ItemIsReference = false,
TypeNameHandling = TypeNameHandling.None,
ShouldSerialize = x =>
{
IThumbnails thumb = (IThumbnails)x;
return thumb?.Images?.ContainsKey(id) == true;
},
ValueProvider = new ThumbnailProvider(id)
});
}
// foreach ((int id, string image) in Images.ImageName)
// {
// properties.Add(new JsonProperty
// {
// DeclaringType = type,
// PropertyName = image.ToLower(),
// UnderlyingName = image,
// PropertyType = typeof(string),
// Readable = true,
// Writable = false,
// ItemIsReference = false,
// TypeNameHandling = TypeNameHandling.None,
// ShouldSerialize = x =>
// {
// IThumbnails thumb = (IThumbnails)x;
// return thumb?.Images?.ContainsKey(id) == true;
// },
// ValueProvider = new ThumbnailProvider(id)
// });
// }
return properties;
}
@ -128,23 +128,22 @@ namespace Kyoo.Core.Api
/// <inheritdoc />
public void SetValue(object target, object value)
{
if (target is not IThumbnails thumb)
throw new ArgumentException($"The given object is not an Thumbnail.");
thumb.Images[_imageIndex] = value as string;
throw new NotSupportedException();
}
/// <inheritdoc />
public object GetValue(object target)
{
string slug = (target as IResource)?.Slug ?? (target as ICustomTypeDescriptor)?.GetComponentName();
if (target is not IThumbnails thumb
|| slug == null
|| string.IsNullOrEmpty(thumb.Images?.GetValueOrDefault(_imageIndex)))
return null;
string type = target is ICustomTypeDescriptor descriptor
? descriptor.GetClassName()
: target.GetType().Name;
return $"/{type}/{slug}/{Images.ImageName[_imageIndex]}".ToLowerInvariant();
return null;
// string slug = (target as IResource)?.Slug ?? (target as ICustomTypeDescriptor)?.GetComponentName();
// if (target is not IThumbnails thumb
// || slug == null
// || string.IsNullOrEmpty(thumb.Images?.GetValueOrDefault(_imageIndex)))
// return null;
// string type = target is ICustomTypeDescriptor descriptor
// ? descriptor.GetClassName()
// : target.GetType().Name;
// return $"/{type}/{slug}/{Images.ImageName[_imageIndex]}".ToLowerInvariant();
}
}
}

View File

@ -20,6 +20,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -80,11 +81,6 @@ namespace Kyoo.Postgresql
/// </summary>
public DbSet<Studio> Studios { get; set; }
/// <summary>
/// All providers of Kyoo. See <see cref="Provider"/>.
/// </summary>
public DbSet<Provider> Providers { get; set; }
/// <summary>
/// The list of registered users.
/// </summary>
@ -108,17 +104,6 @@ namespace Kyoo.Postgresql
/// </remarks>
public DbSet<LibraryItem> LibraryItems { get; set; }
/// <summary>
/// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID"/>.
/// </summary>
/// <typeparam name="T">The metadata of this type will be returned.</typeparam>
/// <returns>A queryable of metadata ids for a type.</returns>
public DbSet<MetadataID> MetadataIds<T>()
where T : class, IMetadata
{
return Set<MetadataID>(MetadataName<T>());
}
/// <summary>
/// Add a many to many link between two resources.
/// </summary>
@ -194,17 +179,33 @@ namespace Kyoo.Postgresql
/// </summary>
/// <param name="modelBuilder">The database model builder</param>
/// <typeparam name="T">The type to add metadata to.</typeparam>
private void _HasMetadata<T>(ModelBuilder modelBuilder)
private static void _HasMetadata<T>(ModelBuilder modelBuilder)
where T : class, IMetadata
{
modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>())
.HasKey(MetadataID.PrimaryKey);
// TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
// modelBuilder.Entity<T>()
// .OwnsOne(x => x.ExternalIDs, x =>
// {
// x.ToJson();
// });
modelBuilder.Entity<T>()
.Property(x => x.ExternalId)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Dictionary<string, MetadataID>>(v, (JsonSerializerOptions)null)
)
.HasColumnType("json");
}
modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>())
.HasOne<T>()
.WithMany(x => x.ExternalIDs)
.HasForeignKey(x => x.ResourceID)
.OnDelete(DeleteBehavior.Cascade);
private static void _HasImages<T>(ModelBuilder modelBuilder)
where T : class, IThumbnails
{
modelBuilder.Entity<T>()
.OwnsOne(x => x.Poster);
modelBuilder.Entity<T>()
.OwnsOne(x => x.Thumbnail);
modelBuilder.Entity<T>()
.OwnsOne(x => x.Logo);
}
/// <summary>
@ -269,7 +270,6 @@ namespace Kyoo.Postgresql
.WithMany(x => x.Shows)
.OnDelete(DeleteBehavior.SetNull);
_HasManyToMany<Library, Provider>(modelBuilder, x => x.Providers, x => x.Libraries);
_HasManyToMany<Library, Collection>(modelBuilder, x => x.Collections, x => x.Libraries);
_HasManyToMany<Library, Show>(modelBuilder, x => x.Shows, x => x.Libraries);
_HasManyToMany<Collection, Show>(modelBuilder, x => x.Shows, x => x.Collections);
@ -287,6 +287,15 @@ namespace Kyoo.Postgresql
_HasMetadata<People>(modelBuilder);
_HasMetadata<Studio>(modelBuilder);
_HasImages<LibraryItem>(modelBuilder);
_HasImages<Collection>(modelBuilder);
_HasImages<Show>(modelBuilder);
_HasImages<Season>(modelBuilder);
_HasImages<Episode>(modelBuilder);
_HasImages<People>(modelBuilder);
modelBuilder.Entity<User>().OwnsOne(x => x.Logo);
modelBuilder.Entity<WatchedEpisode>()
.HasKey(x => new { User = x.UserID, Episode = x.EpisodeID });
@ -294,7 +303,6 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Season>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Episode>().Property(x => x.Slug).IsRequired();
@ -319,9 +327,6 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<Studio>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Provider>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Season>()
.HasIndex(x => new { x.ShowID, x.SeasonNumber })
.IsUnique();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,772 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
public partial class AddBlurhash : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationHelper.DropLibraryItemsView(migrationBuilder);
migrationBuilder.DropTable(
name: "collection_metadata_id");
migrationBuilder.DropTable(
name: "episode_metadata_id");
migrationBuilder.DropTable(
name: "link_library_provider");
migrationBuilder.DropTable(
name: "people_metadata_id");
migrationBuilder.DropTable(
name: "season_metadata_id");
migrationBuilder.DropTable(
name: "show_metadata_id");
migrationBuilder.DropTable(
name: "studio_metadata_id");
migrationBuilder.DropTable(
name: "providers");
migrationBuilder.DropColumn(
name: "images",
table: "users");
migrationBuilder.DropColumn(
name: "images",
table: "shows");
migrationBuilder.DropColumn(
name: "images",
table: "seasons");
migrationBuilder.DropColumn(
name: "images",
table: "people");
migrationBuilder.DropColumn(
name: "images",
table: "episodes");
migrationBuilder.DropColumn(
name: "images",
table: "collections");
migrationBuilder.AddColumn<string>(
name: "logo_blurhash",
table: "users",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_source",
table: "users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "external_id",
table: "studios",
type: "json",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "external_id",
table: "shows",
type: "json",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_blurhash",
table: "shows",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_source",
table: "shows",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_blurhash",
table: "shows",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_source",
table: "shows",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_blurhash",
table: "shows",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_source",
table: "shows",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "trailer",
table: "shows",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "external_id",
table: "seasons",
type: "json",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_blurhash",
table: "seasons",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_source",
table: "seasons",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_blurhash",
table: "seasons",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_source",
table: "seasons",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_blurhash",
table: "seasons",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_source",
table: "seasons",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "external_id",
table: "people",
type: "json",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_blurhash",
table: "people",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_source",
table: "people",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_blurhash",
table: "people",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_source",
table: "people",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_blurhash",
table: "people",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_source",
table: "people",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "external_id",
table: "episodes",
type: "json",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_blurhash",
table: "episodes",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_source",
table: "episodes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_blurhash",
table: "episodes",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_source",
table: "episodes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_blurhash",
table: "episodes",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_source",
table: "episodes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "external_id",
table: "collections",
type: "json",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_blurhash",
table: "collections",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "logo_source",
table: "collections",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_blurhash",
table: "collections",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "poster_source",
table: "collections",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_blurhash",
table: "collections",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail_source",
table: "collections",
type: "text",
nullable: true);
MigrationHelper.CreateLibraryItemsView(migrationBuilder);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
MigrationHelper.DropLibraryItemsView(migrationBuilder);
migrationBuilder.DropColumn(
name: "logo_blurhash",
table: "users");
migrationBuilder.DropColumn(
name: "logo_source",
table: "users");
migrationBuilder.DropColumn(
name: "external_id",
table: "studios");
migrationBuilder.DropColumn(
name: "external_id",
table: "shows");
migrationBuilder.DropColumn(
name: "logo_blurhash",
table: "shows");
migrationBuilder.DropColumn(
name: "logo_source",
table: "shows");
migrationBuilder.DropColumn(
name: "poster_blurhash",
table: "shows");
migrationBuilder.DropColumn(
name: "poster_source",
table: "shows");
migrationBuilder.DropColumn(
name: "thumbnail_blurhash",
table: "shows");
migrationBuilder.DropColumn(
name: "thumbnail_source",
table: "shows");
migrationBuilder.DropColumn(
name: "trailer",
table: "shows");
migrationBuilder.DropColumn(
name: "external_id",
table: "seasons");
migrationBuilder.DropColumn(
name: "logo_blurhash",
table: "seasons");
migrationBuilder.DropColumn(
name: "logo_source",
table: "seasons");
migrationBuilder.DropColumn(
name: "poster_blurhash",
table: "seasons");
migrationBuilder.DropColumn(
name: "poster_source",
table: "seasons");
migrationBuilder.DropColumn(
name: "thumbnail_blurhash",
table: "seasons");
migrationBuilder.DropColumn(
name: "thumbnail_source",
table: "seasons");
migrationBuilder.DropColumn(
name: "external_id",
table: "people");
migrationBuilder.DropColumn(
name: "logo_blurhash",
table: "people");
migrationBuilder.DropColumn(
name: "logo_source",
table: "people");
migrationBuilder.DropColumn(
name: "poster_blurhash",
table: "people");
migrationBuilder.DropColumn(
name: "poster_source",
table: "people");
migrationBuilder.DropColumn(
name: "thumbnail_blurhash",
table: "people");
migrationBuilder.DropColumn(
name: "thumbnail_source",
table: "people");
migrationBuilder.DropColumn(
name: "external_id",
table: "episodes");
migrationBuilder.DropColumn(
name: "logo_blurhash",
table: "episodes");
migrationBuilder.DropColumn(
name: "logo_source",
table: "episodes");
migrationBuilder.DropColumn(
name: "poster_blurhash",
table: "episodes");
migrationBuilder.DropColumn(
name: "poster_source",
table: "episodes");
migrationBuilder.DropColumn(
name: "thumbnail_blurhash",
table: "episodes");
migrationBuilder.DropColumn(
name: "thumbnail_source",
table: "episodes");
migrationBuilder.DropColumn(
name: "external_id",
table: "collections");
migrationBuilder.DropColumn(
name: "logo_blurhash",
table: "collections");
migrationBuilder.DropColumn(
name: "logo_source",
table: "collections");
migrationBuilder.DropColumn(
name: "poster_blurhash",
table: "collections");
migrationBuilder.DropColumn(
name: "poster_source",
table: "collections");
migrationBuilder.DropColumn(
name: "thumbnail_blurhash",
table: "collections");
migrationBuilder.DropColumn(
name: "thumbnail_source",
table: "collections");
migrationBuilder.AddColumn<Dictionary<int, string>>(
name: "images",
table: "users",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<Dictionary<int, string>>(
name: "images",
table: "shows",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<Dictionary<int, string>>(
name: "images",
table: "seasons",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<Dictionary<int, string>>(
name: "images",
table: "people",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<Dictionary<int, string>>(
name: "images",
table: "episodes",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<Dictionary<int, string>>(
name: "images",
table: "collections",
type: "jsonb",
nullable: true);
migrationBuilder.CreateTable(
name: "providers",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
name = table.Column<string>(type: "text", nullable: true),
slug = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_providers", x => x.id);
});
migrationBuilder.CreateTable(
name: "collection_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_collection_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_collection_metadata_id_collections_collection_id",
column: x => x.resource_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_collection_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "episode_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_episode_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_episode_metadata_id_episodes_episode_id",
column: x => x.resource_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_episode_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_library_provider",
columns: table => new
{
library_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_provider", x => new { x.library_id, x.provider_id });
table.ForeignKey(
name: "fk_link_library_provider_libraries_library_id",
column: x => x.library_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_provider_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "people_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_people_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_people_metadata_id_people_people_id",
column: x => x.resource_id,
principalTable: "people",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_people_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "season_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_season_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_season_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_season_metadata_id_seasons_season_id",
column: x => x.resource_id,
principalTable: "seasons",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "show_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_show_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_show_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_show_metadata_id_shows_show_id",
column: x => x.resource_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "studio_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_studio_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_studio_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_studio_metadata_id_studios_studio_id",
column: x => x.resource_id,
principalTable: "studios",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_collection_metadata_id_provider_id",
table: "collection_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_episode_metadata_id_provider_id",
table: "episode_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_provider_provider_id",
table: "link_library_provider",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_people_metadata_id_provider_id",
table: "people_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_providers_slug",
table: "providers",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_season_metadata_id_provider_id",
table: "season_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_show_metadata_id_provider_id",
table: "show_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_studio_metadata_id_provider_id",
table: "studio_metadata_id",
column: "provider_id");
MigrationHelper.CreateLibraryItemsView(migrationBuilder);
}
}
}

View File

@ -33,11 +33,6 @@ namespace Kyoo.Postgresql
/// </summary>
public class PostgresContext : DatabaseContext
{
/// <summary>
/// The connection string to use.
/// </summary>
private readonly string _connection;
/// <summary>
/// Is this instance in debug mode?
/// </summary>
@ -78,7 +73,6 @@ namespace Kyoo.Postgresql
/// <param name="debugMode">Is this instance in debug mode?</param>
public PostgresContext(string connection, bool debugMode)
{
_connection = connection;
_debugMode = debugMode;
}
@ -116,31 +110,6 @@ namespace Kyoo.Postgresql
.Property(x => x.ExtraData)
.HasColumnType("jsonb");
modelBuilder.Entity<LibraryItem>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Collection>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Show>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Season>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Episode>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<People>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Provider>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<User>()
.Property(x => x.Images)
.HasColumnType("jsonb");
base.OnModelCreating(modelBuilder);
}

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Reflection;
using Kyoo.Abstractions.Controllers;
@ -87,7 +86,6 @@ namespace Kyoo.Swagger
x.IsNullableRaw = false;
x.Type = JsonObjectType.String | JsonObjectType.Integer;
}));
document.SchemaProcessors.Add(new ThumbnailProcessor());
document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
{

View File

@ -1,49 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Models;
using NJsonSchema;
using NJsonSchema.Generation;
namespace Kyoo.Swagger
{
/// <summary>
/// An operation processor to add computed fields of <see cref="IThumbnails"/>.
/// </summary>
public class ThumbnailProcessor : ISchemaProcessor
{
/// <inheritdoc />
public void Process(SchemaProcessorContext context)
{
if (!context.ContextualType.OriginalType.IsAssignableTo(typeof(IThumbnails)))
return;
foreach ((int _, string imageP) in Images.ImageName)
{
string image = imageP.ToLower()[0] + imageP[1..];
context.Schema.Properties.Add(image, new JsonSchemaProperty
{
Type = JsonObjectType.String,
IsNullableRaw = true,
Description = $"An url to the {image} of this resource. If this resource does not have an image, " +
$"the link will be null. If the kyoo's instance is not capable of handling this kind of image " +
$"for the specific resource, this field won't be present."
});
}
}
}
}

View File

@ -37,22 +37,20 @@ namespace Kyoo.Tests.Database
{
Context = new PostgresTestContext(postgres, output);
ProviderRepository provider = new(_NewContext());
LibraryRepository library = new(_NewContext(), provider);
CollectionRepository collection = new(_NewContext(), provider);
LibraryRepository library = new(_NewContext());
CollectionRepository collection = new(_NewContext());
GenreRepository genre = new(_NewContext());
StudioRepository studio = new(_NewContext(), provider);
PeopleRepository people = new(_NewContext(), provider,
StudioRepository studio = new(_NewContext());
PeopleRepository people = new(_NewContext(),
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository));
ShowRepository show = new(_NewContext(), studio, people, genre, provider);
SeasonRepository season = new(_NewContext(), show, provider);
ShowRepository show = new(_NewContext(), studio, people, genre);
SeasonRepository season = new(_NewContext(), show);
LibraryItemRepository libraryItem = new(_NewContext(),
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
EpisodeRepository episode = new(_NewContext(), show, provider);
EpisodeRepository episode = new(_NewContext(), show);
UserRepository user = new(_NewContext());
LibraryManager = new LibraryManager(new IBaseRepository[] {
provider,
library,
libraryItem,
collection,

View File

@ -78,7 +78,7 @@ namespace Kyoo.Tests.Database
public async Task CreateWithExternalIdTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.ExternalIDs = new[]
collection.ExternalId = new[]
{
new MetadataID
{
@ -96,10 +96,10 @@ namespace Kyoo.Tests.Database
await _repository.Create(collection);
Collection retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs);
Assert.Equal(2, retrieved.ExternalIDs.Count);
KAssert.DeepEqual(collection.ExternalIDs.First(), retrieved.ExternalIDs.First());
KAssert.DeepEqual(collection.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(collection.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(collection.ExternalId.Last(), retrieved.ExternalId.Last());
}
[Fact]
@ -123,7 +123,7 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -136,7 +136,7 @@ namespace Kyoo.Tests.Database
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
@ -147,7 +147,7 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalIDs = new List<MetadataID>
value.ExternalId = new List<MetadataID>
{
new()
{
@ -161,14 +161,14 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
value.ExternalId.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
@ -179,7 +179,7 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();

View File

@ -206,7 +206,7 @@ namespace Kyoo.Tests.Database
public async Task CreateWithExternalIdTest()
{
Episode value = TestSample.GetNew<Episode>();
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -224,10 +224,10 @@ namespace Kyoo.Tests.Database
await _repository.Create(value);
Episode retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs);
Assert.Equal(2, retrieved.ExternalIDs.Count);
KAssert.DeepEqual(value.ExternalIDs.First(), retrieved.ExternalIDs.First());
KAssert.DeepEqual(value.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last());
}
[Fact]
@ -251,7 +251,7 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -264,7 +264,7 @@ namespace Kyoo.Tests.Database
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
@ -275,7 +275,7 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalIDs = new List<MetadataID>
value.ExternalId = new List<MetadataID>
{
new()
{
@ -289,14 +289,14 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
value.ExternalId.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
@ -307,7 +307,7 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();

View File

@ -52,7 +52,7 @@ namespace Kyoo.Tests.Database
public async Task CreateWithExternalIdTest()
{
People value = TestSample.GetNew<People>();
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -70,10 +70,10 @@ namespace Kyoo.Tests.Database
await _repository.Create(value);
People retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs);
Assert.Equal(2, retrieved.ExternalIDs.Count);
KAssert.DeepEqual(value.ExternalIDs.First(), retrieved.ExternalIDs.First());
KAssert.DeepEqual(value.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last());
}
[Fact]
@ -97,7 +97,7 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -110,7 +110,7 @@ namespace Kyoo.Tests.Database
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
@ -121,7 +121,7 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.ExternalIDs = new List<MetadataID>
value.ExternalId = new List<MetadataID>
{
new()
{
@ -135,14 +135,14 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
value.ExternalId.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
@ -153,7 +153,7 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();

View File

@ -93,7 +93,7 @@ namespace Kyoo.Tests.Database
public async Task CreateWithExternalIdTest()
{
Season season = TestSample.GetNew<Season>();
season.ExternalIDs = new[]
season.ExternalId = new[]
{
new MetadataID
{
@ -111,10 +111,10 @@ namespace Kyoo.Tests.Database
await _repository.Create(season);
Season retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs);
Assert.Equal(2, retrieved.ExternalIDs.Count);
KAssert.DeepEqual(season.ExternalIDs.First(), retrieved.ExternalIDs.First());
KAssert.DeepEqual(season.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(season.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(season.ExternalId.Last(), retrieved.ExternalId.Last());
}
[Fact]
@ -138,7 +138,7 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
Season value = await _repository.Get(TestSample.Get<Season>().Slug);
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -151,7 +151,7 @@ namespace Kyoo.Tests.Database
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
@ -162,7 +162,7 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
Season value = await _repository.Get(TestSample.Get<Season>().Slug);
value.ExternalIDs = new List<MetadataID>
value.ExternalId = new List<MetadataID>
{
new()
{
@ -176,14 +176,14 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
value.ExternalId.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
@ -194,7 +194,7 @@ namespace Kyoo.Tests.Database
{
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();

View File

@ -180,7 +180,7 @@ namespace Kyoo.Tests.Database
public async Task EditExternalIDsTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.ExternalIDs = new[]
value.ExternalId = new[]
{
new MetadataID
{
@ -192,19 +192,19 @@ namespace Kyoo.Tests.Database
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(
value.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug }),
edited.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug }));
value.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }),
edited.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }));
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(
value.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug }),
show.ExternalIDs.Select(x => new { x.DataID, x.Provider.Slug }));
value.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }),
show.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }));
}
[Fact]
@ -225,7 +225,7 @@ namespace Kyoo.Tests.Database
Assert.Equal("reset", edited.Slug);
Assert.Equal("Reset", edited.Title);
Assert.Null(edited.Aliases);
Assert.Null(edited.ExternalIDs);
Assert.Null(edited.ExternalId);
Assert.Null(edited.People);
Assert.Null(edited.Genres);
Assert.Null(edited.Studio);
@ -237,7 +237,7 @@ namespace Kyoo.Tests.Database
Show expected = TestSample.Get<Show>();
expected.ID = 0;
expected.Slug = "created-relation-test";
expected.ExternalIDs = new[]
expected.ExternalId = new[]
{
new MetadataID
{
@ -269,7 +269,7 @@ namespace Kyoo.Tests.Database
await using DatabaseContext context = Repositories.Context.New();
Show retrieved = await context.Shows
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.Include(x => x.Genres)
.Include(x => x.People)
@ -300,7 +300,7 @@ namespace Kyoo.Tests.Database
Show expected = TestSample.Get<Show>();
expected.ID = 0;
expected.Slug = "created-relation-test";
expected.ExternalIDs = new[]
expected.ExternalId = new[]
{
new MetadataID
{
@ -312,12 +312,12 @@ namespace Kyoo.Tests.Database
KAssert.DeepEqual(expected, created);
await using DatabaseContext context = Repositories.Context.New();
Show retrieved = await context.Shows
.Include(x => x.ExternalIDs)
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync(x => x.ID == created.ID);
KAssert.DeepEqual(expected, retrieved);
Assert.Single(retrieved.ExternalIDs);
Assert.Equal("ID", retrieved.ExternalIDs.First().DataID);
Assert.Single(retrieved.ExternalId);
Assert.Equal("ID", retrieved.ExternalId.First().DataID);
}
[Fact]

View File

@ -350,14 +350,9 @@ namespace Kyoo.Tests
people.ID = 0;
context.People.Add(people);
Provider provider = Get<Provider>();
provider.ID = 0;
context.Providers.Add(provider);
Library library = Get<Library>();
library.ID = 0;
library.Collections = new List<Collection> { collection };
library.Providers = new List<Provider> { provider };
context.Libraries.Add(library);
User user = Get<User>();

View File

@ -353,69 +353,54 @@ namespace Kyoo.Tests.Utility
{
ID = 5,
Name = "merged",
Images = new Dictionary<int, string>
{
[Images.Logo] = "logo",
[Images.Poster] = "poster"
}
};
Collection collection2 = new()
{
Name = "test",
Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster",
[Images.Thumbnail] = "thumbnails"
}
};
Collection ret = Merger.Complete(collection, collection2);
Assert.True(ReferenceEquals(collection, ret));
Assert.Equal(5, ret.ID);
Assert.Equal("test", ret.Name);
Assert.Null(ret.Slug);
Assert.Equal(3, ret.Images.Count);
Assert.Equal("new-poster", ret.Images[Images.Poster]);
Assert.Equal("thumbnails", ret.Images[Images.Thumbnail]);
Assert.Equal("logo", ret.Images[Images.Logo]);
}
[Fact]
public void CompleteDictionaryOutParam()
{
Dictionary<int, string> first = new()
Dictionary<string, string> first = new()
{
[Images.Logo] = "logo",
[Images.Poster] = "poster"
["logo"] = "logo",
["poster"] = "poster"
};
Dictionary<int, string> second = new()
Dictionary<string, string> second = new()
{
[Images.Poster] = "new-poster",
[Images.Thumbnail] = "thumbnails"
["poster"] = "new-poster",
["thumbnail"] = "thumbnails"
};
IDictionary<int, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Equal(3, ret.Count);
Assert.Equal("new-poster", ret[Images.Poster]);
Assert.Equal("thumbnails", ret[Images.Thumbnail]);
Assert.Equal("logo", ret[Images.Logo]);
Assert.Equal("new-poster", ret["poster"]);
Assert.Equal("thumbnails", ret["thumbnail"]);
Assert.Equal("logo", ret["logo"]);
}
[Fact]
public void CompleteDictionaryEqualTest()
{
Dictionary<int, string> first = new()
Dictionary<string, string> first = new()
{
[Images.Poster] = "poster"
["poster"] = "poster"
};
Dictionary<int, string> second = new()
Dictionary<string, string> second = new()
{
[Images.Poster] = "new-poster",
["poster"] = "new-poster",
};
IDictionary<int, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Single(ret);
Assert.Equal("new-poster", ret[Images.Poster]);
Assert.Equal("new-poster", ret["poster"]);
}
private class TestMergeSetter
@ -473,81 +458,81 @@ namespace Kyoo.Tests.Utility
[Fact]
public void MergeDictionaryNullValue()
{
Dictionary<int, string> first = new()
Dictionary<string, string> first = new()
{
[Images.Logo] = "logo",
[Images.Poster] = null
["logo"] = "logo",
["poster"] = null
};
Dictionary<int, string> second = new()
Dictionary<string, string> second = new()
{
[Images.Poster] = "new-poster",
[Images.Thumbnail] = "thumbnails"
["poster"] = "new-poster",
["thumbnail"] = "thumbnails"
};
IDictionary<int, string> ret = Merger.MergeDictionaries(first, second, out bool changed);
IDictionary<string, string> ret = Merger.MergeDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Equal(3, ret.Count);
Assert.Equal("new-poster", ret[Images.Poster]);
Assert.Equal("thumbnails", ret[Images.Thumbnail]);
Assert.Equal("logo", ret[Images.Logo]);
Assert.Equal("new-poster", ret["poster"]);
Assert.Equal("thumbnails", ret["thumbnail"]);
Assert.Equal("logo", ret["logo"]);
}
[Fact]
public void MergeDictionaryNullValueNoChange()
{
Dictionary<int, string> first = new()
Dictionary<string, string> first = new()
{
[Images.Logo] = "logo",
[Images.Poster] = null
["logo"] = "logo",
["poster"] = null
};
Dictionary<int, string> second = new()
Dictionary<string, string> second = new()
{
[Images.Poster] = null,
["poster"] = null,
};
IDictionary<int, string> ret = Merger.MergeDictionaries(first, second, out bool changed);
IDictionary<string, string> ret = Merger.MergeDictionaries(first, second, out bool changed);
Assert.False(changed);
Assert.Equal(2, ret.Count);
Assert.Null(ret[Images.Poster]);
Assert.Equal("logo", ret[Images.Logo]);
Assert.Null(ret["poster"]);
Assert.Equal("logo", ret["logo"]);
}
[Fact]
public void CompleteDictionaryNullValue()
{
Dictionary<int, string> first = new()
Dictionary<string, string> first = new()
{
[Images.Logo] = "logo",
[Images.Poster] = null
["logo"] = "logo",
["poster"] = null
};
Dictionary<int, string> second = new()
Dictionary<string, string> second = new()
{
[Images.Poster] = "new-poster",
[Images.Thumbnail] = "thumbnails"
["poster"] = "new-poster",
["thumbnail"] = "thumbnails"
};
IDictionary<int, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Equal(3, ret.Count);
Assert.Equal("new-poster", ret[Images.Poster]);
Assert.Equal("thumbnails", ret[Images.Thumbnail]);
Assert.Equal("logo", ret[Images.Logo]);
Assert.Equal("new-poster", ret["poster"]);
Assert.Equal("thumbnails", ret["thumbnail"]);
Assert.Equal("logo", ret["logo"]);
}
[Fact]
public void CompleteDictionaryNullValueNoChange()
{
Dictionary<int, string> first = new()
Dictionary<string, string> first = new()
{
[Images.Logo] = "logo",
[Images.Poster] = null
["logo"] = "logo",
["poster"] = null
};
Dictionary<int, string> second = new()
Dictionary<string, string> second = new()
{
[Images.Poster] = null,
["poster"] = null,
};
IDictionary<int, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
IDictionary<string, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
Assert.False(changed);
Assert.Equal(2, ret.Count);
Assert.Null(ret[Images.Poster]);
Assert.Equal("logo", ret[Images.Logo]);
Assert.Null(ret["poster"]);
Assert.Equal("logo", ret["logo"]);
}
}
}

View File

@ -39,7 +39,7 @@ namespace Kyoo.Tests.Utility
Assert.True(KUtility.IsPropertyExpression(member));
Assert.True(KUtility.IsPropertyExpression(memberCast));
Expression<Func<Show, object>> call = x => x.GetID("test");
Expression<Func<Show, object>> call = x => x.ToString();
Assert.False(KUtility.IsPropertyExpression(call));
}

View File

@ -102,7 +102,7 @@ const Menu = <AsProps,>({
}),
])}
>
<ScrollView {...css([])}>
<ScrollView>
<IconButton
icon={Close}
color={theme.colors.black}

View File

@ -44,7 +44,5 @@ class Episode:
"images": {
"1": self.thumbnail,
},
# TODO: The back has bad external id support, we disable it for now
"external_ids": None,
"show": None,
}