Merge pull request #32 from AnonymusRaccoon/the-moviedb

Adding TheMovieDB provider
This commit is contained in:
Zoe Roux 2021-08-06 13:01:00 +02:00 committed by GitHub
commit 387bf4c34d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 7577 additions and 4844 deletions

View File

@ -24,7 +24,7 @@ jobs:
- name: Build - name: Build
run: | run: |
dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' -p:CopyLocalLockFileAssemblies=true dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' -p:CopyLocalLockFileAssemblies=true
cp ./Kyoo.Common/bin/Debug/net5.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll ./Kyoo.Tests/bin/Debug/net5.0/ cp ./Kyoo.Common/bin/Debug/net5.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll ./tests/Kyoo.Tests/bin/Debug/net5.0/
- name: Test - name: Test
run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover' run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover'
env: env:
@ -33,7 +33,7 @@ jobs:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
- name: Sanitize coverage output - name: Sanitize coverage output
if: ${{ always() }} if: ${{ always() }}
run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml run: sed -i "s'$(pwd)'.'" tests/Kyoo.Tests/coverage.opencover.xml
- name: Upload coverage report - name: Upload coverage report
if: ${{ always() }} if: ${{ always() }}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

View File

@ -2,11 +2,25 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A class wrapping a value that will be set after the completion of the task it is related to.
/// </summary>
/// <remarks>
/// This class replace the use of an out parameter on a task since tasks and out can't be combined.
/// </remarks>
/// <typeparam name="T">The type of the value</typeparam>
public class AsyncRef<T>
{
/// <summary>
/// The value that will be set before the completion of the task.
/// </summary>
public T Value { get; set; }
}
/// <summary> /// <summary>
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers) /// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
/// </summary> /// </summary>
@ -43,6 +57,16 @@ namespace Kyoo.Controllers
/// <returns>A reader to read the file.</returns> /// <returns>A reader to read the file.</returns>
public Task<Stream> GetReader([NotNull] string path); public Task<Stream> GetReader([NotNull] string path);
/// <summary>
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
/// To return files from an http endpoint, use <see cref="FileResult"/>.
/// </summary>
/// <param name="path">The path of the file</param>
/// <param name="mime">The mime type of the opened file.</param>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <returns>A reader to read the file.</returns>
public Task<Stream> GetReader([NotNull] string path, AsyncRef<string> mime);
/// <summary> /// <summary>
/// Create a new file at <paramref name="path"></paramref>. /// Create a new file at <paramref name="path"></paramref>.
/// </summary> /// </summary>
@ -81,12 +105,13 @@ namespace Kyoo.Controllers
public Task<bool> Exists([NotNull] string path); public Task<bool> Exists([NotNull] string path);
/// <summary> /// <summary>
/// Get the extra directory of a show. /// Get the extra directory of a resource <typeparamref name="T"/>.
/// This method is in this system to allow a filesystem to use a different metadata policy for one. /// This method is in this system to allow a filesystem to use a different metadata policy for one.
/// It can be useful if the filesystem is readonly. /// It can be useful if the filesystem is readonly.
/// </summary> /// </summary>
/// <param name="show">The show to proceed</param> /// <param name="resource">The resource to proceed</param>
/// <returns>The extra directory of the show</returns> /// <typeparam name="T">The type of the resource.</typeparam>
public string GetExtraDirectory([NotNull] Show show); /// <returns>The extra directory of the resource.</returns>
public Task<string> GetExtraDirectory<T>([NotNull] T resource);
} }
} }

View File

@ -225,42 +225,51 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="obj">The source object.</param> /// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param> /// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam> /// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam> /// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns> /// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/> /// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String)"/> /// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load(IResource, string)"/> /// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member) Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource where T : class, IResource
where T2 : class, IResource, new(); where T2 : class, IResource;
/// <summary> /// <summary>
/// Load a collection of related resource /// Load a collection of related resource
/// </summary> /// </summary>
/// <param name="obj">The source object.</param> /// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param> /// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam> /// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam> /// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns> /// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/> /// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String)"/> /// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load(IResource, string)"/> /// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member) Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource where T : class, IResource
where T2 : class, new(); where T2 : class;
/// <summary> /// <summary>
/// Load a related resource by it's name /// Load a related resource by it's name
/// </summary> /// </summary>
/// <param name="obj">The source object.</param> /// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param> /// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam> /// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <see cref="obj"/></returns> /// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/> /// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/> /// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load(IResource, string)"/> /// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T>([NotNull] T obj, string memberName) Task<T> Load<T>([NotNull] T obj, string memberName, bool force = false)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -268,10 +277,13 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="obj">The source object.</param> /// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param> /// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/> /// <param name="force">
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/> /// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// <seealso cref="Load{T}(T, System.String)"/> /// </param>
Task Load([NotNull] IResource obj, string memberName); /// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
Task Load([NotNull] IResource obj, string memberName, bool force = false);
/// <summary> /// <summary>
/// Get items (A wrapper arround shows or collections) from a library. /// Get items (A wrapper arround shows or collections) from a library.

View File

@ -590,10 +590,10 @@ namespace Kyoo.Controllers
/// <param name="limit">Pagination information (where to start and how many to get)</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> /// <typeparam name="T">The type of metadata to retrieve</typeparam>
/// <returns>A filtered list of external ids.</returns> /// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null, Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID<T>> sort = default, Sort<MetadataID> sort = default,
Pagination limit = default) Pagination limit = default)
where T : class, IResource; where T : class, IMetadata;
/// <summary> /// <summary>
/// Get a list of external ids that match all filters /// Get a list of external ids that match all filters
@ -602,11 +602,11 @@ namespace Kyoo.Controllers
/// <param name="sort">A sort by expression</param> /// <param name="sort">A sort by expression</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param> /// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns> /// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID<T>>> GetMetadataID<T>([Optional] Expression<Func<MetadataID<T>, bool>> where, Task<ICollection<MetadataID>> GetMetadataID<T>([Optional] Expression<Func<MetadataID, bool>> where,
Expression<Func<MetadataID<T>, object>> sort, Expression<Func<MetadataID, object>> sort,
Pagination limit = default Pagination limit = default
) where T : class, IResource ) where T : class, IMetadata
=> GetMetadataID(where, new Sort<MetadataID<T>>(sort), limit); => GetMetadataID<T>(where, new Sort<MetadataID>(sort), limit);
} }
/// <summary> /// <summary>

View File

@ -1,5 +1,4 @@
using System; using Kyoo.Models;
using Kyoo.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -23,37 +22,17 @@ namespace Kyoo.Controllers
/// <typeparam name="T">The type of the item</typeparam> /// <typeparam name="T">The type of the item</typeparam>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
Task<bool> DownloadImages<T>([NotNull] T item, bool alwaysDownload = false) Task<bool> DownloadImages<T>([NotNull] T item, bool alwaysDownload = false)
where T : IResource; where T : IThumbnails;
/// <summary> /// <summary>
/// Retrieve the local path of the poster of the given item. /// Retrieve the local path of an image of the given item.
/// </summary> /// </summary>
/// <param name="item">The item to retrieve the poster from.</param> /// <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> /// <typeparam name="T">The type of the item</typeparam>
/// <exception cref="NotSupportedException">If the type does not have a poster</exception> /// <returns>The path of the image for the given resource or null if it does not exists.</returns>
/// <returns>The path of the poster for the given resource (it might or might not exists).</returns> Task<string> GetImagePath<T>([NotNull] T item, int imageID)
Task<string> GetPoster<T>([NotNull] T item) where T : IThumbnails;
where T : IResource;
/// <summary>
/// Retrieve the local path of the logo of the given item.
/// </summary>
/// <param name="item">The item to retrieve the logo from.</param>
/// <typeparam name="T">The type of the item</typeparam>
/// <exception cref="NotSupportedException">If the type does not have a logo</exception>
/// <returns>The path of the logo for the given resource (it might or might not exists).</returns>
Task<string> GetLogo<T>([NotNull] T item)
where T : IResource;
/// <summary>
/// Retrieve the local path of the thumbnail of the given item.
/// </summary>
/// <param name="item">The item to retrieve the thumbnail from.</param>
/// <typeparam name="T">The type of the item</typeparam>
/// <exception cref="NotSupportedException">If the type does not have a thumbnail</exception>
/// <returns>The path of the thumbnail for the given resource (it might or might not exists).</returns>
Task<string> GetThumbnail<T>([NotNull] T item)
where T : IResource;
} }
} }

View File

@ -162,34 +162,6 @@ namespace Kyoo.Controllers
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber); return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
} }
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
where T : class, IResource
where T2 : class, IResource, new()
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member));
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource
where T2 : class, new()
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member));
}
/// <inheritdoc />
public async Task<T> Load<T>(T obj, string memberName)
where T : class, IResource
{
await Load(obj as IResource, memberName);
return obj;
}
/// <summary> /// <summary>
/// Set relations between to objects. /// Set relations between to objects.
/// </summary> /// </summary>
@ -211,11 +183,46 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public Task Load(IResource obj, string memberName) public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource
where T2 : class, IResource
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member), force);
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource
where T2 : class
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member), force);
}
/// <inheritdoc />
public async Task<T> Load<T>(T obj, string memberName, bool force = false)
where T : class, IResource
{
await Load(obj as IResource, memberName, force);
return obj;
}
/// <inheritdoc />
public Task Load(IResource obj, string memberName, bool force = false)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
object existingValue = obj.GetType()
.GetProperties()
.FirstOrDefault(x => string.Equals(x.Name, memberName, StringComparison.InvariantCultureIgnoreCase))
?.GetValue(obj);
if (existingValue != null && !force)
return Task.CompletedTask;
return (obj, member: memberName) switch return (obj, member: memberName) switch
{ {
(Library l, nameof(Library.Providers)) => ProviderRepository (Library l, nameof(Library.Providers)) => ProviderRepository
@ -231,7 +238,12 @@ namespace Kyoo.Controllers
.Then(x => l.Collections = x), .Then(x => l.Collections = x),
(Collection c, nameof(Library.Shows)) => ShowRepository (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)) .GetAll(x => x.Collections.Any(y => y.ID == obj.ID))
.Then(x => c.Shows = x), .Then(x => c.Shows = x),
@ -241,9 +253,9 @@ namespace Kyoo.Controllers
(Show s, nameof(Show.ExternalIDs)) => SetRelation(s, (Show s, nameof(Show.ExternalIDs)) => SetRelation(s,
ProviderRepository.GetMetadataID<Show>(x => x.FirstID == obj.ID), ProviderRepository.GetMetadataID<Show>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y, (x, y) => x.ExternalIDs = y,
(x, y) => { x.First = y; x.FirstID = y.ID; }), (x, y) => { x.ResourceID = y.ID; }),
(Show s, nameof(Show.Genres)) => GenreRepository (Show s, nameof(Show.Genres)) => GenreRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) .GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
@ -281,9 +293,9 @@ namespace Kyoo.Controllers
(Season s, nameof(Season.ExternalIDs)) => SetRelation(s, (Season s, nameof(Season.ExternalIDs)) => SetRelation(s,
ProviderRepository.GetMetadataID<Season>(x => x.FirstID == obj.ID), ProviderRepository.GetMetadataID<Season>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y, (x, y) => x.ExternalIDs = y,
(x, y) => { x.First = y; x.FirstID = y.ID; }), (x, y) => { x.ResourceID = y.ID; }),
(Season s, nameof(Season.Episodes)) => SetRelation(s, (Season s, nameof(Season.Episodes)) => SetRelation(s,
EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), EpisodeRepository.GetAll(x => x.Season.ID == obj.ID),
@ -300,9 +312,9 @@ namespace Kyoo.Controllers
(Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e, (Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e,
ProviderRepository.GetMetadataID<Episode>(x => x.FirstID == obj.ID), ProviderRepository.GetMetadataID<Episode>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y, (x, y) => x.ExternalIDs = y,
(x, y) => { x.First = y; x.FirstID = y.ID; }), (x, y) => { x.ResourceID = y.ID; }),
(Episode e, nameof(Episode.Tracks)) => SetRelation(e, (Episode e, nameof(Episode.Tracks)) => SetRelation(e,
TrackRepository.GetAll(x => x.Episode.ID == obj.ID), TrackRepository.GetAll(x => x.Episode.ID == obj.ID),
@ -344,11 +356,16 @@ namespace Kyoo.Controllers
.GetAll(x => x.Studio.ID == obj.ID) .GetAll(x => x.Studio.ID == obj.ID)
.Then(x => s.Shows = x), .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, (People p, nameof(People.ExternalIDs)) => SetRelation(p,
ProviderRepository.GetMetadataID<People>(x => x.FirstID == obj.ID), ProviderRepository.GetMetadataID<People>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y, (x, y) => x.ExternalIDs = y,
(x, y) => { x.First = y; x.FirstID = y.ID; }), (x, y) => { x.ResourceID = y.ID; }),
(People p, nameof(People.Roles)) => PeopleRepository (People p, nameof(People.Roles)) => PeopleRepository
.GetFromPeople(obj.ID) .GetFromPeople(obj.ID)

View File

@ -16,13 +16,11 @@
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<DefineConstants>ENABLE_INTERNAL_LINKS</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac" Version="6.2.0" /> <PackageReference Include="Autofac" Version="6.2.0" />
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />

View File

@ -1,13 +0,0 @@
using System;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Common.Models.Attributes
{
/// <summary>
/// An attribute to mark Link properties on resource.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse]
public class LinkAttribute : SerializeIgnoreAttribute { }
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
@ -18,7 +19,7 @@ namespace Kyoo.Models
/// A type union between <see cref="Show"/> and <see cref="Collection"/>. /// A type union between <see cref="Show"/> and <see cref="Collection"/>.
/// This is used to list content put inside a library. /// This is used to list content put inside a library.
/// </summary> /// </summary>
public class LibraryItem : IResource public class LibraryItem : IResource, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -53,12 +54,16 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public DateTime? EndAir { get; set; } public DateTime? EndAir { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this item's poster. /// The path of this item's poster.
/// By default, the http path for this poster is returned from the public API. /// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")] public string Poster { get; set; } [SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")]
public string Poster => Images?.GetValueOrDefault(Models.Images.Poster);
/// <summary> /// <summary>
/// The type of this item (ether a collection, a show or a movie). /// The type of this item (ether a collection, a show or a movie).
@ -84,7 +89,7 @@ namespace Kyoo.Models
Status = show.Status; Status = show.Status;
StartAir = show.StartAir; StartAir = show.StartAir;
EndAir = show.EndAir; EndAir = show.EndAir;
Poster = show.Poster; Images = show.Images;
Type = show.IsMovie ? ItemType.Movie : ItemType.Show; Type = show.IsMovie ? ItemType.Movie : ItemType.Show;
} }
@ -101,7 +106,7 @@ namespace Kyoo.Models
Status = Models.Status.Unknown; Status = Models.Status.Unknown;
StartAir = null; StartAir = null;
EndAir = null; EndAir = null;
Poster = collection.Poster; Images = collection.Images;
Type = ItemType.Collection; Type = ItemType.Collection;
} }
@ -117,7 +122,7 @@ namespace Kyoo.Models
Status = x.Status, Status = x.Status,
StartAir = x.StartAir, StartAir = x.StartAir,
EndAir = x.EndAir, EndAir = x.EndAir,
Poster= x.Poster, Images = x.Images,
Type = x.IsMovie ? ItemType.Movie : ItemType.Show Type = x.IsMovie ? ItemType.Movie : ItemType.Show
}; };
@ -133,7 +138,7 @@ namespace Kyoo.Models
Status = Models.Status.Unknown, Status = Models.Status.Unknown,
StartAir = null, StartAir = null,
EndAir = null, EndAir = null,
Poster = x.Poster, Images = x.Images,
Type = ItemType.Collection Type = ItemType.Collection
}; };
} }

View File

@ -1,163 +0,0 @@
using System;
using System.Linq.Expressions;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A class representing a link between two resources.
/// </summary>
/// <remarks>
/// Links should only be used on the data layer and not on other application code.
/// </remarks>
public class Link
{
/// <summary>
/// The ID of the first item of the link.
/// The first item of the link should be the one to own the link.
/// </summary>
public int FirstID { get; set; }
/// <summary>
/// The ID of the second item of this link
/// The second item of the link should be the owned resource.
/// </summary>
public int SecondID { get; set; }
/// <summary>
/// Create a new typeless <see cref="Link"/>.
/// </summary>
public Link() {}
/// <summary>
/// Create a new typeless <see cref="Link"/> with two IDs.
/// </summary>
/// <param name="firstID">The ID of the first resource</param>
/// <param name="secondID">The ID of the second resource</param>
public Link(int firstID, int secondID)
{
FirstID = firstID;
SecondID = secondID;
}
/// <summary>
/// Create a new typeless <see cref="Link"/> between two resources.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
public Link(IResource first, IResource second)
{
FirstID = first.ID;
SecondID = second.ID;
}
/// <summary>
/// Create a new typed link between two resources.
/// This method can be used instead of the constructor to make use of generic parameters deduction.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
/// <typeparam name="T">The type of the first resource</typeparam>
/// <typeparam name="T2">The type of the second resource</typeparam>
/// <returns>A newly created typed link with both resources</returns>
public static Link<T, T2> Create<T, T2>(T first, T2 second)
where T : class, IResource
where T2 : class, IResource
{
return new(first, second);
}
/// <summary>
/// Create a new typed link between two resources without storing references to resources.
/// This is the same as <see cref="Create{T,T2}"/> but this method does not set <see cref="Link{T1,T2}.First"/>
/// and <see cref="Link{T1,T2}.Second"/> fields. Only IDs are stored and not references.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
/// <typeparam name="T">The type of the first resource</typeparam>
/// <typeparam name="T2">The type of the second resource</typeparam>
/// <returns>A newly created typed link with both resources</returns>
public static Link<T, T2> UCreate<T, T2>(T first, T2 second)
where T : class, IResource
where T2 : class, IResource
{
return new(first, second, true);
}
/// <summary>
/// The expression to retrieve the unique ID of a Link. This is an aggregate of the two resources IDs.
/// </summary>
public static Expression<Func<Link, object>> PrimaryKey
{
get
{
return x => new {First = x.FirstID, Second = x.SecondID};
}
}
}
/// <summary>
/// A strongly typed link between two resources.
/// </summary>
/// <typeparam name="T1">The type of the first resource</typeparam>
/// <typeparam name="T2">The type of the second resource</typeparam>
public class Link<T1, T2> : Link
where T1 : class, IResource
where T2 : class, IResource
{
/// <summary>
/// A reference of the first resource.
/// </summary>
[SerializeIgnore] public T1 First { get; set; }
/// <summary>
/// A reference to the second resource.
/// </summary>
[SerializeIgnore] public T2 Second { get; set; }
/// <summary>
/// Create a new, empty, typed <see cref="Link{T1,T2}"/>.
/// </summary>
public Link() {}
/// <summary>
/// Create a new typed link with two resources.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
/// <param name="privateItems">
/// True if no reference to resources should be kept, false otherwise.
/// The default is false (references are kept).
/// </param>
public Link(T1 first, T2 second, bool privateItems = false)
: base(first, second)
{
if (privateItems)
return;
First = first;
Second = second;
}
/// <summary>
/// Create a new typed link with IDs only.
/// </summary>
/// <param name="firstID">The ID of the first resource</param>
/// <param name="secondID">The ID of the second resource</param>
public Link(int firstID, int secondID)
: base(firstID, secondID)
{ }
/// <summary>
/// The expression to retrieve the unique ID of a typed Link. This is an aggregate of the two resources IDs.
/// </summary>
public new static Expression<Func<Link<T1, T2>, object>> PrimaryKey
{
get
{
return x => new {First = x.FirstID, Second = x.SecondID};
}
}
}
}

View File

@ -1,15 +1,29 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary> /// <summary>
/// ID and link of an item on an external provider. /// ID and link of an item on an external provider.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public class MetadataID
public class MetadataID<T> : Link<T, Provider>
where T : class, IResource
{ {
/// <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> /// <summary>
/// The ID of the resource on the external provider. /// The ID of the resource on the external provider.
/// </summary> /// </summary>
@ -20,21 +34,12 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public string Link { get; set; } public string Link { get; set; }
/// <summary>
/// A shortcut to access the provider of this metadata.
/// Unlike the <see cref="Link{T, T2}.Second"/> property, this is serializable.
/// </summary>
public Provider Provider => Second;
/// <summary> /// <summary>
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs. /// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
/// </summary> /// </summary>
public new static Expression<Func<MetadataID<T>, object>> PrimaryKey public static Expression<Func<MetadataID, object>> PrimaryKey
{ {
get get { return x => new { First = x.ResourceID, Second = x.ProviderID }; }
{
return x => new {First = x.FirstID, Second = x.SecondID};
}
} }
} }
} }

View File

@ -17,8 +17,8 @@ namespace Kyoo.Models
public string Slug => ForPeople ? Show.Slug : People.Slug; public string Slug => ForPeople ? Show.Slug : People.Slug;
/// <summary> /// <summary>
/// Should this role be used as a Show substitute (the value is <c>false</c>) or /// Should this role be used as a Show substitute (the value is <c>true</c>) or
/// as a People substitute (the value is <c>true</c>). /// as a People substitute (the value is <c>false</c>).
/// </summary> /// </summary>
public bool ForPeople { get; set; } public bool ForPeople { get; set; }

View File

@ -1,5 +1,5 @@
using System.Collections.Generic; using System;
using Kyoo.Common.Models.Attributes; using System.Collections.Generic;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
@ -8,7 +8,7 @@ namespace Kyoo.Models
/// A class representing collections of <see cref="Show"/>. /// A class representing collections of <see cref="Show"/>.
/// A collection can also be stored in a <see cref="Library"/>. /// A collection can also be stored in a <see cref="Library"/>.
/// </summary> /// </summary>
public class Collection : IResource public class Collection : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -21,12 +21,17 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this poster. /// The path of this poster.
/// By default, the http path for this poster is returned from the public API. /// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; } [SerializeAs("{HOST}/api/collection/{Slug}/poster")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Poster => Images?.GetValueOrDefault(Models.Images.Poster);
/// <summary> /// <summary>
/// The description of this collection. /// The description of this collection.
@ -43,17 +48,7 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; } [LoadableRelation] public ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS /// <inheritdoc />
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// <summary>
/// The internal link between this collection and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Collection, Show>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this collection and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
#endif
} }
} }

View File

@ -10,7 +10,7 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// A class to represent a single show's episode. /// A class to represent a single show's episode.
/// </summary> /// </summary>
public class Episode : IResource public class Episode : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -74,9 +74,13 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[SerializeIgnore] public int? SeasonID { get; set; } [SerializeIgnore] public int? SeasonID { get; set; }
/// <summary> /// <summary>
/// The season that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>. /// The season that contains this episode.
/// This can be null if the season is unknown and the episode is only identified by it's <see cref="AbsoluteNumber"/>. /// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary> /// </summary>
/// <remarks>
/// This can be null if the season is unknown and the episode is only identified
/// by it's <see cref="AbsoluteNumber"/>.
/// </remarks>
[LoadableRelation(nameof(SeasonID))] public Season Season { get; set; } [LoadableRelation(nameof(SeasonID))] public Season Season { get; set; }
/// <summary> /// <summary>
@ -85,7 +89,7 @@ namespace Kyoo.Models
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
/// <summary> /// <summary>
/// The number of this episode is it's season. /// The number of this episode in it's season.
/// </summary> /// </summary>
public int? EpisodeNumber { get; set; } public int? EpisodeNumber { get; set; }
@ -99,12 +103,17 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[SerializeIgnore] public string Path { get; set; } [SerializeIgnore] public string Path { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this episode's thumbnail. /// The path of this episode's thumbnail.
/// By default, the http path for the thumbnail is returned from the public API. /// By default, the http path for the thumbnail is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; } [SerializeAs("{HOST}/api/episodes/{Slug}/thumbnail")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Thumb => Images?.GetValueOrDefault(Models.Images.Thumbnail);
/// <summary> /// <summary>
/// The title of this episode. /// The title of this episode.
@ -121,10 +130,8 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
/// <summary> /// <inheritdoc />
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information. [EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Episode>> ExternalIDs { get; set; }
/// <summary> /// <summary>
/// The list of tracks this episode has. This lists video, audio and subtitles available. /// The list of tracks this episode has. This lists video, audio and subtitles available.

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
@ -25,13 +24,6 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; } [LoadableRelation] public ICollection<Show> Shows { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// The internal link between this genre and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Show, Genre>> ShowLinks { get; set; }
#endif
/// <summary> /// <summary>
/// Create a new, empty <see cref="Genre"/>. /// Create a new, empty <see cref="Genre"/>.
/// </summary> /// </summary>

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// An interface applied to resources containing external metadata.
/// </summary>
public interface IMetadata
{
/// <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;
}
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using Kyoo.Controllers;
namespace Kyoo.Models
{
/// <summary>
/// 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.
/// The string value should be a path supported by the <see cref="IFileSystem"/>.
/// </summary>
/// <remarks>
/// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/>
/// </remarks>
public Dictionary<int, string> Images { get; set; }
// TODO remove Posters properties add them via the json serializer for every IThumbnails
}
/// <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;
/// <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;
/// <summary>
/// A logo is a small image representing the resource.
/// </summary>
public const int Logo = 2;
/// <summary>
/// A video of a few minutes that tease the content.
/// </summary>
public const int Trailer = 3;
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
@ -39,22 +38,5 @@ namespace Kyoo.Models
/// The list of collections in this library. /// The list of collections in this library.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Collection> Collections { get; set; } [LoadableRelation] public ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// The internal link between this library and provider in the <see cref="Providers"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
/// <summary>
/// The internal link between this library and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Show>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this library and collection in the <see cref="Collections"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
#endif
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
@ -6,7 +7,7 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>. /// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
/// </summary> /// </summary>
public class People : IResource public class People : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -19,17 +20,20 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this poster. /// The path of this poster.
/// By default, the http path for this poster is returned from the public API. /// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } [SerializeAs("{HOST}/api/people/{Slug}/poster")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Poster => Images?.GetValueOrDefault(Models.Images.Poster);
/// <summary> /// <inheritdoc />
/// The link to metadata providers that this person has. See <see cref="MetadataID{T}"/> for more information. [EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<People>> ExternalIDs { get; set; }
/// <summary> /// <summary>
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information. /// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.

View File

@ -1,5 +1,5 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
@ -9,7 +9,7 @@ namespace Kyoo.Models
/// This class contains metadata about <see cref="IMetadataProvider"/>. /// This class contains metadata about <see cref="IMetadataProvider"/>.
/// You can have providers even if you don't have the corresponding <see cref="IMetadataProvider"/>. /// You can have providers even if you don't have the corresponding <see cref="IMetadataProvider"/>.
/// </summary> /// </summary>
public class Provider : IResource public class Provider : IResource, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -22,30 +22,23 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this provider's logo. /// The path of this provider's logo.
/// By default, the http path for this logo is returned from the public API. /// By default, the http path for this logo is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; } [SerializeAs("{HOST}/api/providers/{Slug}/logo")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
/// <summary> public string Logo => Images?.GetValueOrDefault(Models.Images.Logo);
/// The extension of the logo. This is used for http responses.
/// </summary>
[SerializeIgnore] public string LogoExtension { get; set; }
/// <summary> /// <summary>
/// The list of libraries that uses this provider. /// The list of libraries that uses this provider.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; } [LoadableRelation] public ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// The internal link between this provider and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
#endif
/// <summary> /// <summary>
/// Create a new, default, <see cref="Provider"/> /// Create a new, default, <see cref="Provider"/>
/// </summary> /// </summary>
@ -61,7 +54,10 @@ namespace Kyoo.Models
{ {
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);
Name = name; Name = name;
Logo = logo; Images = new Dictionary<int, string>
{
[Models.Images.Logo] = logo
};
} }
} }
} }

View File

@ -10,7 +10,7 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// A season of a <see cref="Show"/>. /// A season of a <see cref="Show"/>.
/// </summary> /// </summary>
public class Season : IResource public class Season : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -45,7 +45,8 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[SerializeIgnore] public int ShowID { get; set; } [SerializeIgnore] public int ShowID { get; set; }
/// <summary> /// <summary>
/// The show that contains this season. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>. /// The show that contains this season.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary> /// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; } [LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
@ -74,17 +75,20 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this poster. /// The path of this poster.
/// By default, the http path for this poster is returned from the public API. /// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; } [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Poster => Images?.GetValueOrDefault(Models.Images.Poster);
/// <summary> /// <inheritdoc />
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information. [EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Season>> ExternalIDs { get; set; }
/// <summary> /// <summary>
/// The list of episodes that this season contains. /// The list of episodes that this season contains.

View File

@ -1,8 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
@ -11,7 +8,7 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// A series or a movie. /// A series or a movie.
/// </summary> /// </summary>
public class Show : IResource, IOnMerge public class Show : IResource, IMetadata, IOnMerge, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -49,7 +46,8 @@ namespace Kyoo.Models
/// An URL to a trailer. This could be any path supported by the <see cref="IFileSystem"/>. /// An URL to a trailer. This could be any path supported by the <see cref="IFileSystem"/>.
/// </summary> /// </summary>
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file. /// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
public string TrailerUrl { get; set; } [Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string TrailerUrl => Images?.GetValueOrDefault(Models.Images.Trailer);
/// <summary> /// <summary>
/// The date this show started airing. It can be null if this is unknown. /// The date this show started airing. It can be null if this is unknown.
@ -63,43 +61,51 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public DateTime? EndAir { get; set; } public DateTime? EndAir { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The path of this show's poster. /// The path of this show's poster.
/// By default, the http path for this poster is returned from the public API. /// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; } [SerializeAs("{HOST}/api/shows/{Slug}/poster")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Poster => Images?.GetValueOrDefault(Models.Images.Poster);
/// <summary> /// <summary>
/// The path of this show's logo. /// The path of this show's logo.
/// By default, the http path for this logo is returned from the public API. /// By default, the http path for this logo is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; } [SerializeAs("{HOST}/api/shows/{Slug}/logo")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Logo => Images?.GetValueOrDefault(Models.Images.Logo);
/// <summary> /// <summary>
/// The path of this show's backdrop. /// The path of this show's backdrop.
/// By default, the http path for this backdrop is returned from the public API. /// By default, the http path for this backdrop is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; } [SerializeAs("{HOST}/api/shows/{Slug}/backdrop")]
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string Backdrop => Images?.GetValueOrDefault(Models.Images.Thumbnail);
/// <summary> /// <summary>
/// True if this show represent a movie, false otherwise. /// True if this show represent a movie, false otherwise.
/// </summary> /// </summary>
public bool IsMovie { get; set; } public bool IsMovie { get; set; }
/// <summary> /// <inheritdoc />
/// The link to metadata providers that this show has. See <see cref="MetadataID{T}"/> for more information. [EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Show>> ExternalIDs { get; set; }
/// <summary> /// <summary>
/// The ID of the Studio that made this show. /// The ID of the Studio that made this show.
/// </summary> /// </summary>
[SerializeIgnore] public int? StudioID { get; set; } [SerializeIgnore] public int? StudioID { get; set; }
/// <summary> /// <summary>
/// The Studio that made this show. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>. /// The Studio that made this show.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary> /// </summary>
[LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; } [LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; }
@ -135,41 +141,9 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Collection> Collections { get; set; } [LoadableRelation] public ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// The internal link between this show and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Show>> LibraryLinks { get; set; }
/// <summary>
/// The internal link between this show and collections in the <see cref="Collections"/> list.
/// </summary>
[Link] public ICollection<Link<Collection, Show>> CollectionLinks { get; set; }
/// <summary>
/// The internal link between this show and genres in the <see cref="Genres"/> list.
/// </summary>
[Link] public ICollection<Link<Show, Genre>> GenreLinks { get; set; }
#endif
/// <summary>
/// Retrieve the internal provider's ID of a show using it's provider slug.
/// </summary>
/// <remarks>This method will never return anything if the <see cref="ExternalIDs"/> are not loaded.</remarks>
/// <param name="provider">The slug of the provider</param>
/// <returns>The <see cref="MetadataID{T}.DataID"/> field of the asked provider.</returns>
[CanBeNull]
public string GetID(string provider)
{
return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID;
}
/// <inheritdoc /> /// <inheritdoc />
public void OnMerge(object merged) public void OnMerge(object merged)
{ {
if (ExternalIDs != null)
foreach (MetadataID<Show> id in ExternalIDs)
id.First = this;
if (People != null) if (People != null)
foreach (PeopleRole link in People) foreach (PeopleRole link in People)
link.Show = this; link.Show = this;

View File

@ -6,7 +6,7 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// A studio that make shows. /// A studio that make shows.
/// </summary> /// </summary>
public class Studio : IResource public class Studio : IResource, IMetadata
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -24,6 +24,9 @@ namespace Kyoo.Models
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; } [LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <inheritdoc />
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// <summary> /// <summary>
/// Create a new, empty, <see cref="Studio"/>. /// Create a new, empty, <see cref="Studio"/>.
/// </summary> /// </summary>

View File

@ -1,12 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary> /// <summary>
/// A single user of the app. /// A single user of the app.
/// </summary> /// </summary>
public class User : IResource public class User : IResource, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
@ -39,6 +38,9 @@ namespace Kyoo.Models
/// </summary> /// </summary>
public Dictionary<string, string> ExtraData { get; set; } public Dictionary<string, string> ExtraData { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The list of shows the user has finished. /// The list of shows the user has finished.
/// </summary> /// </summary>
@ -48,20 +50,28 @@ namespace Kyoo.Models
/// The list of episodes the user is watching (stopped in progress or the next episode of the show) /// The list of episodes the user is watching (stopped in progress or the next episode of the show)
/// </summary> /// </summary>
public ICollection<WatchedEpisode> CurrentlyWatching { get; set; } public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// Links between Users and Shows.
/// </summary>
[Link] public ICollection<Link<User, Show>> ShowLinks { get; set; }
#endif
} }
/// <summary> /// <summary>
/// Metadata of episode currently watching by an user /// Metadata of episode currently watching by an user
/// </summary> /// </summary>
public class WatchedEpisode : Link<User, Episode> public class WatchedEpisode
{ {
/// <summary>
/// The ID of the user that started watching this episode.
/// </summary>
public int UserID { get; set; }
/// <summary>
/// The ID of the episode started.
/// </summary>
public int EpisodeID { get; set; }
/// <summary>
/// The <see cref="Episode"/> started.
/// </summary>
public Episode Episode { get; set; }
/// <summary> /// <summary>
/// Where the player has stopped watching the episode (between 0 and 100). /// Where the player has stopped watching the episode (between 0 and 100).
/// </summary> /// </summary>

View File

@ -22,12 +22,13 @@ namespace Kyoo
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param> /// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param> /// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
/// <returns>The two list merged as an array</returns> /// <returns>The two list merged as an array</returns>
public static T[] MergeLists<T>(IEnumerable<T> first, [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
IEnumerable<T> second, public static T[] MergeLists<T>([CanBeNull] IEnumerable<T> first,
Func<T, T, bool> isEqual = null) [CanBeNull] IEnumerable<T> second,
[CanBeNull] Func<T, T, bool> isEqual = null)
{ {
if (first == null) if (first == null)
return second.ToArray(); return second?.ToArray();
if (second == null) if (second == null)
return first.ToArray(); return first.ToArray();
if (isEqual == null) if (isEqual == null)
@ -36,6 +37,98 @@ namespace Kyoo
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray(); return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
} }
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
/// <seealso cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2},out bool)"/>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second)
{
return MergeDictionaries(first, second, out bool _);
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second,
out bool hasChanged)
{
if (first == null)
{
hasChanged = true;
return second;
}
hasChanged = false;
if (second == null)
return first;
foreach ((T key, T2 value) in second)
hasChanged |= first.TryAdd(key, value);
return first;
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
/// </summary>
/// <remarks>
/// The only difference in this function compared to
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2}, out bool)"/>
/// is the way <paramref name="hasChanged"/> is calculated and the order of the arguments.
/// <code>
/// MergeDictionaries(first, second);
/// </code>
/// will do the same thing as
/// <code>
/// CompleteDictionaries(second, first, out bool _);
/// </code>
/// </remarks>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>
/// A dictionary with the missing elements of <paramref name="second"/>
/// set to those of <paramref name="first"/>.
/// </returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> CompleteDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second,
out bool hasChanged)
{
if (first == null)
{
hasChanged = true;
return second;
}
hasChanged = false;
if (second == null)
return first;
hasChanged = second.Any(x => !x.Value.Equals(first[x.Key]));
foreach ((T key, T2 value) in first)
second.TryAdd(key, value);
return second;
}
/// <summary> /// <summary>
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute /// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/> /// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
@ -63,16 +156,34 @@ namespace Kyoo
} }
/// <summary> /// <summary>
/// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. /// Set every non-default values of seconds to the corresponding property of second.
/// Dictionaries are handled like anonymous objects with a property per key/pair value
/// (see
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2})"/>
/// for more details).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/> /// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary> /// </summary>
/// <param name="first">The object to complete</param> /// <remarks>
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param> /// This does the opposite of <see cref="Merge{T}"/>.
/// <param name="where">Filter fields that will be merged</param> /// </remarks>
/// <example>
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
/// </example>
/// <param name="first">
/// The object to complete
/// </param>
/// <param name="second">
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
/// </param>
/// <param name="where">
/// Filter fields that will be merged
/// </param>
/// <typeparam name="T">Fields of T will be completed</typeparam> /// <typeparam name="T">Fields of T will be completed</typeparam>
/// <returns><see cref="first"/></returns> /// <returns><see cref="first"/></returns>
/// <exception cref="ArgumentNullException">If first is null</exception> /// <exception cref="ArgumentNullException">If first is null</exception>
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null) public static T Complete<T>([NotNull] T first,
[CanBeNull] T second,
[InstantHandle] Func<PropertyInfo, bool> where = null)
{ {
if (first == null) if (first == null)
throw new ArgumentNullException(nameof(first)); throw new ArgumentNullException(nameof(first));
@ -93,7 +204,26 @@ namespace Kyoo
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
?? property.PropertyType.GetClrDefault(); ?? property.PropertyType.GetClrDefault();
if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first)))
continue;
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments;
object[] parameters = {
property.GetValue(first),
value,
false
};
object newDictionary = Utility.RunGenericMethod<object>(
typeof(Merger),
nameof(CompleteDictionaries),
dictionaryTypes,
parameters);
if ((bool)parameters[2])
property.SetValue(first, newDictionary);
}
else
property.SetValue(first, value); property.SetValue(first, value);
} }
@ -103,17 +233,28 @@ namespace Kyoo
} }
/// <summary> /// <summary>
/// An advanced <see cref="Complete{T}"/> function.
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>. /// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
/// Enumerable will be merged (concatenated). /// Enumerable will be merged (concatenated) and Dictionaries too.
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>. /// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary> /// </summary>
/// <param name="first">The object to complete</param> /// <example>
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param> /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}
/// </example>
/// <param name="first">
/// The object to complete
/// </param>
/// <param name="second">
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
/// </param>
/// <param name="where">
/// Filter fields that will be merged
/// </param>
/// <typeparam name="T">Fields of T will be merged</typeparam> /// <typeparam name="T">Fields of T will be merged</typeparam>
/// <returns><see cref="first"/></returns> /// <returns><see cref="first"/></returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T Merge<T>([CanBeNull] T first, [CanBeNull] T second) public static T Merge<T>([CanBeNull] T first,
[CanBeNull] T second,
[InstantHandle] Func<PropertyInfo, bool> where = null)
{ {
if (first == null) if (first == null)
return second; return second;
@ -125,6 +266,9 @@ namespace Kyoo
.Where(x => x.CanRead && x.CanWrite .Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties) foreach (PropertyInfo property in properties)
{ {
object oldValue = property.GetValue(first); object oldValue = property.GetValue(first);
@ -133,6 +277,23 @@ namespace Kyoo
if (oldValue?.Equals(defaultValue) != false) if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue); property.SetValue(first, newValue);
else if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments;
object[] parameters = {
oldValue,
newValue,
false
};
object newDictionary = Utility.RunGenericMethod<object>(
typeof(Merger),
nameof(MergeDictionaries),
dictionaryTypes,
parameters);
if ((bool)parameters[2])
property.SetValue(first, newDictionary);
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string)) && property.PropertyType != typeof(string))
{ {

View File

@ -325,7 +325,7 @@ namespace Kyoo
if (types.Length < 1) if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(null, args.ToArray()); return (T)method.MakeGenericMethod(types).Invoke(null, args);
} }
/// <summary> /// <summary>

View File

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
@ -60,6 +62,7 @@ namespace Kyoo
/// All providers of Kyoo. See <see cref="Provider"/>. /// All providers of Kyoo. See <see cref="Provider"/>.
/// </summary> /// </summary>
public DbSet<Provider> Providers { get; set; } public DbSet<Provider> Providers { get; set; }
/// <summary> /// <summary>
/// The list of registered users. /// The list of registered users.
/// </summary> /// </summary>
@ -84,28 +87,34 @@ namespace Kyoo
public DbSet<LibraryItem> LibraryItems { get; set; } public DbSet<LibraryItem> LibraryItems { get; set; }
/// <summary> /// <summary>
/// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID{T}"/>. /// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The metadata of this type will be returned.</typeparam> /// <typeparam name="T">The metadata of this type will be returned.</typeparam>
/// <returns>A queryable of metadata ids for a type.</returns> /// <returns>A queryable of metadata ids for a type.</returns>
public DbSet<MetadataID<T>> MetadataIds<T>() public DbSet<MetadataID> MetadataIds<T>()
where T : class, IResource where T : class, IMetadata
{ {
return Set<MetadataID<T>>(); return Set<MetadataID>(MetadataName<T>());
} }
/// <summary> /// <summary>
/// Get a generic link between two resource types. /// Add a many to many link between two resources.
/// </summary> /// </summary>
/// <remarks>Types are order dependant. You can't inverse the order. Please always put the owner first.</remarks> /// <remarks>Types are order dependant. You can't inverse the order. Please always put the owner first.</remarks>
/// <param name="first">The ID of the first resource.</param>
/// <param name="second">The ID of the second resource.</param>
/// <typeparam name="T1">The first resource type of the relation. It is the owner of the second</typeparam> /// <typeparam name="T1">The first resource type of the relation. It is the owner of the second</typeparam>
/// <typeparam name="T2">The second resource type of the relation. It is the contained resource.</typeparam> /// <typeparam name="T2">The second resource type of the relation. It is the contained resource.</typeparam>
/// <returns>All links between the two types.</returns> public async Task AddLinks<T1, T2>(int first, int second)
public DbSet<Link<T1, T2>> Links<T1, T2>()
where T1 : class, IResource where T1 : class, IResource
where T2 : class, IResource where T2 : class, IResource
{ {
return Set<Link<T1, T2>>(); await Set<Dictionary<string, object>>(LinkName<T1, T2>())
.AddAsync(new Dictionary<string, object>
{
[LinkNameFk<T1>()] = first,
[LinkNameFk<T2>()] = second
});
} }
@ -122,6 +131,32 @@ namespace Kyoo
: base(options) : base(options)
{ } { }
/// <summary>
/// Get the name of the metadata table of the given type.
/// </summary>
/// <typeparam name="T">The type related to the metadata</typeparam>
/// <returns>The name of the table containing the metadata.</returns>
protected abstract string MetadataName<T>()
where T : IMetadata;
/// <summary>
/// Get the name of the link table of the two given types.
/// </summary>
/// <typeparam name="T">The owner type of the relation</typeparam>
/// <typeparam name="T2">The child type of the relation</typeparam>
/// <returns>The name of the table containing the links.</returns>
protected abstract string LinkName<T, T2>()
where T : IResource
where T2 : IResource;
/// <summary>
/// Get the name of a link's foreign key.
/// </summary>
/// <typeparam name="T">The type that will be accessible via the navigation</typeparam>
/// <returns>The name of the foreign key for the given resource.</returns>
protected abstract string LinkNameFk<T>()
where T : IResource;
/// <summary> /// <summary>
/// Set basic configurations (like preventing query tracking) /// Set basic configurations (like preventing query tracking)
/// </summary> /// </summary>
@ -132,6 +167,58 @@ namespace Kyoo
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
} }
/// <summary>
/// Build the metadata model for the given type.
/// </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)
where T : class, IMetadata
{
modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>())
.HasKey(MetadataID.PrimaryKey);
modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>())
.HasOne<T>()
.WithMany(x => x.ExternalIDs)
.HasForeignKey(x => x.ResourceID)
.OnDelete(DeleteBehavior.Cascade);
}
/// <summary>
/// Create a many to many relationship between the two entities.
/// The resulting relationship will have an available <see cref="AddLinks{T1,T2}"/> method.
/// </summary>
/// <param name="modelBuilder">The database model builder</param>
/// <param name="firstNavigation">The first navigation expression from T to T2</param>
/// <param name="secondNavigation">The second navigation expression from T2 to T</param>
/// <typeparam name="T">The owning type of the relationship</typeparam>
/// <typeparam name="T2">The owned type of the relationship</typeparam>
private void _HasManyToMany<T, T2>(ModelBuilder modelBuilder,
Expression<Func<T, IEnumerable<T2>>> firstNavigation,
Expression<Func<T2, IEnumerable<T>>> secondNavigation)
where T : class, IResource
where T2 : class, IResource
{
modelBuilder.Entity<T2>()
.HasMany(secondNavigation)
.WithMany(firstNavigation)
.UsingEntity<Dictionary<string, object>>(
LinkName<T, T2>(),
x => x
.HasOne<T>()
.WithMany()
.HasForeignKey(LinkNameFk<T>())
.OnDelete(DeleteBehavior.Cascade),
x => x
.HasOne<T2>()
.WithMany()
.HasForeignKey(LinkNameFk<T2>())
.OnDelete(DeleteBehavior.Cascade)
);
}
/// <summary> /// <summary>
/// Set database parameters to support every types of Kyoo. /// Set database parameters to support every types of Kyoo.
/// </summary> /// </summary>
@ -140,6 +227,9 @@ namespace Kyoo
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PeopleRole>()
.Ignore(x => x.ForPeople);
modelBuilder.Entity<Show>() modelBuilder.Entity<Show>()
.HasMany(x => x.Seasons) .HasMany(x => x.Seasons)
.WithOne(x => x.Show) .WithOne(x => x.Show)
@ -162,117 +252,26 @@ namespace Kyoo
.WithMany(x => x.Shows) .WithMany(x => x.Shows)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Provider>() _HasManyToMany<Library, Provider>(modelBuilder, x => x.Providers, x => x.Libraries);
.HasMany(x => x.Libraries) _HasManyToMany<Library, Collection>(modelBuilder, x => x.Collections, x => x.Libraries);
.WithMany(x => x.Providers) _HasManyToMany<Library, Show>(modelBuilder, x => x.Shows, x => x.Libraries);
.UsingEntity<Link<Library, Provider>>( _HasManyToMany<Collection, Show>(modelBuilder, x => x.Shows, x => x.Collections);
y => y _HasManyToMany<Show, Genre>(modelBuilder, x => x.Genres, x => x.Shows);
.HasOne(x => x.First)
.WithMany(x => x.ProviderLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, Provider>.PrimaryKey));
modelBuilder.Entity<Collection>()
.HasMany(x => x.Libraries)
.WithMany(x => x.Collections)
.UsingEntity<Link<Library, Collection>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.CollectionLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, Collection>.PrimaryKey));
modelBuilder.Entity<Show>()
.HasMany(x => x.Libraries)
.WithMany(x => x.Shows)
.UsingEntity<Link<Library, Show>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.ShowLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, Show>.PrimaryKey));
modelBuilder.Entity<Show>()
.HasMany(x => x.Collections)
.WithMany(x => x.Shows)
.UsingEntity<Link<Collection, Show>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.ShowLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.CollectionLinks),
y => y.HasKey(Link<Collection, Show>.PrimaryKey));
modelBuilder.Entity<Genre>()
.HasMany(x => x.Shows)
.WithMany(x => x.Genres)
.UsingEntity<Link<Show, Genre>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.GenreLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<Show, Genre>.PrimaryKey));
modelBuilder.Entity<User>() modelBuilder.Entity<User>()
.HasMany(x => x.Watched) .HasMany(x => x.Watched)
.WithMany("users") .WithMany("Users")
.UsingEntity<Link<User, Show>>( .UsingEntity(x => x.ToTable(LinkName<User, Show>()));
y => y
.HasOne(x => x.Second)
.WithMany(),
y => y
.HasOne(x => x.First)
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<User, Show>.PrimaryKey));
modelBuilder.Entity<MetadataID<Show>>() _HasMetadata<Collection>(modelBuilder);
.HasKey(MetadataID<Show>.PrimaryKey); _HasMetadata<Show>(modelBuilder);
modelBuilder.Entity<MetadataID<Show>>() _HasMetadata<Season>(modelBuilder);
.HasOne(x => x.First) _HasMetadata<Episode>(modelBuilder);
.WithMany(x => x.ExternalIDs) _HasMetadata<People>(modelBuilder);
.OnDelete(DeleteBehavior.Cascade); _HasMetadata<Studio>(modelBuilder);
modelBuilder.Entity<MetadataID<Season>>()
.HasKey(MetadataID<Season>.PrimaryKey);
modelBuilder.Entity<MetadataID<Season>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Episode>>()
.HasKey(MetadataID<Episode>.PrimaryKey);
modelBuilder.Entity<MetadataID<Episode>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<People>>()
.HasKey(MetadataID<People>.PrimaryKey);
modelBuilder.Entity<MetadataID<People>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Show>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Season>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Episode>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<People>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Show>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WatchedEpisode>() modelBuilder.Entity<WatchedEpisode>()
.HasKey(x => new {First = x.FirstID, Second = x.SecondID}); .HasKey(x => new { User = x.UserID, Episode = x.EpisodeID });
modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
@ -505,6 +504,23 @@ namespace Kyoo
} }
} }
/// <summary>
/// Return the first resource with the given slug that is currently tracked by this context.
/// This allow one to limit redundant calls to <see cref="IRepository{T}.CreateIfNotExists"/> during the
/// same transaction and prevent fails from EF when two same entities are being tracked.
/// </summary>
/// <param name="slug">The slug of the resource to check</param>
/// <typeparam name="T">The type of entity to check</typeparam>
/// <returns>The local entity representing the resource with the given slug if it exists or null.</returns>
[CanBeNull]
public T LocalEntity<T>(string slug)
where T : class, IResource
{
return ChangeTracker.Entries<T>()
.FirstOrDefault(x => x.Entity.Slug == slug)
?.Entity;
}
/// <summary> /// <summary>
/// Check if the exception is a duplicated exception. /// Check if the exception is a duplicated exception.
/// </summary> /// </summary>
@ -517,14 +533,12 @@ namespace Kyoo
/// </summary> /// </summary>
private void DiscardChanges() private void DiscardChanges()
{ {
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Detached))
&& x.State != EntityState.Detached))
{ {
entry.State = EntityState.Detached; entry.State = EntityState.Detached;
} }
} }
/// <summary> /// <summary>
/// Perform a case insensitive like operation. /// Perform a case insensitive like operation.
/// </summary> /// </summary>

View File

@ -234,16 +234,23 @@ namespace Kyoo.Controllers
finally finally
{ {
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading; Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
Database.ChangeTracker.Clear();
} }
} }
/// <summary> /// <summary>
/// An overridable method to edit relation of a resource. /// An overridable method to edit relation of a resource.
/// </summary> /// </summary>
/// <param name="resource">The non edited resource</param> /// <param name="resource">
/// <param name="changed">The new version of <see cref="resource"/>. This item will be saved on the databse and replace <see cref="resource"/></param> /// The non edited resource
/// <param name="resetOld">A boolean to indicate if all values of resource should be discarded or not.</param> /// </param>
/// <returns></returns> /// <param name="changed">
/// The new version of <see cref="resource"/>.
/// This item will be saved on the database and replace <see cref="resource"/>
/// </param>
/// <param name="resetOld">
/// A boolean to indicate if all values of resource should be discarded or not.
/// </param>
protected virtual Task EditRelations(T resource, T changed, bool resetOld) protected virtual Task EditRelations(T resource, T changed, bool resetOld)
{ {
return Validate(resource); return Validate(resource);
@ -254,7 +261,9 @@ namespace Kyoo.Controllers
/// It is also called on the default implementation of <see cref="EditRelations"/> /// It is also called on the default implementation of <see cref="EditRelations"/>
/// </summary> /// </summary>
/// <param name="resource">The resource that will be saved</param> /// <param name="resource">The resource that will be saved</param>
/// <exception cref="ArgumentException">You can throw this if the resource is illegal and should not be saved.</exception> /// <exception cref="ArgumentException">
/// You can throw this if the resource is illegal and should not be saved.
/// </exception>
protected virtual Task Validate(T resource) protected virtual Task Validate(T resource)
{ {
if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute<ComputedAttribute>() != null) if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute<ComputedAttribute>() != null)

View File

@ -23,7 +23,7 @@ namespace Kyoo.Postgresql.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false), slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true), name = table.Column<string>(type: "text", nullable: true),
poster = table.Column<string>(type: "text", nullable: true), images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
overview = table.Column<string>(type: "text", nullable: true) overview = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
@ -68,7 +68,7 @@ namespace Kyoo.Postgresql.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false), slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true), name = table.Column<string>(type: "text", nullable: true),
poster = table.Column<string>(type: "text", nullable: true) images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -83,8 +83,7 @@ namespace Kyoo.Postgresql.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false), slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true), name = table.Column<string>(type: "text", nullable: true),
logo = table.Column<string>(type: "text", nullable: true), images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
logo_extension = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -116,7 +115,8 @@ namespace Kyoo.Postgresql.Migrations
email = table.Column<string>(type: "text", nullable: true), email = table.Column<string>(type: "text", nullable: true),
password = table.Column<string>(type: "text", nullable: true), password = table.Column<string>(type: "text", nullable: true),
permissions = table.Column<string[]>(type: "text[]", nullable: true), permissions = table.Column<string[]>(type: "text[]", nullable: true),
extra_data = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true) extra_data = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true),
images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -127,71 +127,97 @@ namespace Kyoo.Postgresql.Migrations
name: "link_library_collection", name: "link_library_collection",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), collection_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false) library_id = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_link_library_collection", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_link_library_collection", x => new { x.collection_id, x.library_id });
table.ForeignKey( table.ForeignKey(
name: "fk_link_library_collection_collections_second_id", name: "fk_link_library_collection_collections_collection_id",
column: x => x.second_id, column: x => x.collection_id,
principalTable: "collections", principalTable: "collections",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_link_library_collection_libraries_first_id", name: "fk_link_library_collection_libraries_library_id",
column: x => x.first_id, column: x => x.library_id,
principalTable: "libraries", principalTable: "libraries",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
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( migrationBuilder.CreateTable(
name: "link_library_provider", name: "link_library_provider",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), library_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false) provider_id = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_link_library_provider", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_link_library_provider", x => new { x.library_id, x.provider_id });
table.ForeignKey( table.ForeignKey(
name: "fk_link_library_provider_libraries_first_id", name: "fk_link_library_provider_libraries_library_id",
column: x => x.first_id, column: x => x.library_id,
principalTable: "libraries", principalTable: "libraries",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_link_library_provider_providers_second_id", name: "fk_link_library_provider_providers_provider_id",
column: x => x.second_id, column: x => x.provider_id,
principalTable: "providers", principalTable: "providers",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "metadata_id_people", name: "people_metadata_id",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), resource_id = table.Column<int>(type: "integer", nullable: false),
second_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), data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true) link = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_metadata_id_people", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_people_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey( table.ForeignKey(
name: "fk_metadata_id_people_people_first_id", name: "fk_people_metadata_id_people_people_id",
column: x => x.first_id, column: x => x.resource_id,
principalTable: "people", principalTable: "people",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_metadata_id_people_providers_second_id", name: "fk_people_metadata_id_providers_provider_id",
column: x => x.second_id, column: x => x.provider_id,
principalTable: "providers", principalTable: "providers",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -209,12 +235,9 @@ namespace Kyoo.Postgresql.Migrations
path = table.Column<string>(type: "text", nullable: true), path = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), overview = table.Column<string>(type: "text", nullable: true),
status = table.Column<Status>(type: "status", nullable: false), status = table.Column<Status>(type: "status", nullable: false),
trailer_url = table.Column<string>(type: "text", nullable: true),
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true), start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true), end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
poster = table.Column<string>(type: "text", nullable: true), images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
logo = table.Column<string>(type: "text", nullable: true),
backdrop = table.Column<string>(type: "text", nullable: true),
is_movie = table.Column<bool>(type: "boolean", nullable: false), is_movie = table.Column<bool>(type: "boolean", nullable: false),
studio_id = table.Column<int>(type: "integer", nullable: true) studio_id = table.Column<int>(type: "integer", nullable: true)
}, },
@ -230,24 +253,50 @@ namespace Kyoo.Postgresql.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "link_collection_show", name: "studio_metadata_id",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), resource_id = table.Column<int>(type: "integer", nullable: false),
second_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 => constraints: table =>
{ {
table.PrimaryKey("pk_link_collection_show", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_studio_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey( table.ForeignKey(
name: "fk_link_collection_show_collections_first_id", name: "fk_studio_metadata_id_providers_provider_id",
column: x => x.first_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.CreateTable(
name: "link_collection_show",
columns: table => new
{
collection_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_collection_show", x => new { x.collection_id, x.show_id });
table.ForeignKey(
name: "fk_link_collection_show_collections_collection_id",
column: x => x.collection_id,
principalTable: "collections", principalTable: "collections",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_link_collection_show_shows_second_id", name: "fk_link_collection_show_shows_show_id",
column: x => x.second_id, column: x => x.show_id,
principalTable: "shows", principalTable: "shows",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -257,21 +306,21 @@ namespace Kyoo.Postgresql.Migrations
name: "link_library_show", name: "link_library_show",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), library_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false) show_id = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_link_library_show", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_link_library_show", x => new { x.library_id, x.show_id });
table.ForeignKey( table.ForeignKey(
name: "fk_link_library_show_libraries_first_id", name: "fk_link_library_show_libraries_library_id",
column: x => x.first_id, column: x => x.library_id,
principalTable: "libraries", principalTable: "libraries",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_link_library_show_shows_second_id", name: "fk_link_library_show_shows_show_id",
column: x => x.second_id, column: x => x.show_id,
principalTable: "shows", principalTable: "shows",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -281,21 +330,21 @@ namespace Kyoo.Postgresql.Migrations
name: "link_show_genre", name: "link_show_genre",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), genre_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false) show_id = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_link_show_genre", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_link_show_genre", x => new { x.genre_id, x.show_id });
table.ForeignKey( table.ForeignKey(
name: "fk_link_show_genre_genres_second_id", name: "fk_link_show_genre_genres_genre_id",
column: x => x.second_id, column: x => x.genre_id,
principalTable: "genres", principalTable: "genres",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_link_show_genre_shows_first_id", name: "fk_link_show_genre_shows_show_id",
column: x => x.first_id, column: x => x.show_id,
principalTable: "shows", principalTable: "shows",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -305,59 +354,32 @@ namespace Kyoo.Postgresql.Migrations
name: "link_user_show", name: "link_user_show",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), users_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false) watched_id = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_link_user_show", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_link_user_show", x => new { x.users_id, x.watched_id });
table.ForeignKey( table.ForeignKey(
name: "fk_link_user_show_shows_second_id", name: "fk_link_user_show_shows_watched_id",
column: x => x.second_id, column: x => x.watched_id,
principalTable: "shows", principalTable: "shows",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_link_user_show_users_first_id", name: "fk_link_user_show_users_users_id",
column: x => x.first_id, column: x => x.users_id,
principalTable: "users", principalTable: "users",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "metadata_id_show",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_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_metadata_id_show", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_metadata_id_show_providers_second_id",
column: x => x.second_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_metadata_id_show_shows_first_id",
column: x => x.first_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "people_roles", name: "people_roles",
columns: table => new columns: table => new
{ {
id = table.Column<int>(type: "integer", nullable: false) id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
for_people = table.Column<bool>(type: "boolean", nullable: false),
people_id = table.Column<int>(type: "integer", nullable: false), people_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false), show_id = table.Column<int>(type: "integer", nullable: false),
type = table.Column<string>(type: "text", nullable: true), type = table.Column<string>(type: "text", nullable: true),
@ -393,7 +415,7 @@ namespace Kyoo.Postgresql.Migrations
overview = table.Column<string>(type: "text", nullable: true), overview = table.Column<string>(type: "text", nullable: true),
start_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true), start_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
end_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true), end_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
poster = table.Column<string>(type: "text", nullable: true) images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -406,6 +428,32 @@ namespace Kyoo.Postgresql.Migrations
onDelete: ReferentialAction.Cascade); 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( migrationBuilder.CreateTable(
name: "episodes", name: "episodes",
columns: table => new columns: table => new
@ -419,7 +467,7 @@ namespace Kyoo.Postgresql.Migrations
episode_number = table.Column<int>(type: "integer", nullable: true), episode_number = table.Column<int>(type: "integer", nullable: true),
absolute_number = table.Column<int>(type: "integer", nullable: true), absolute_number = table.Column<int>(type: "integer", nullable: true),
path = table.Column<string>(type: "text", nullable: true), path = table.Column<string>(type: "text", nullable: true),
thumb = table.Column<string>(type: "text", nullable: true), images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
title = table.Column<string>(type: "text", nullable: true), title = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true), overview = table.Column<string>(type: "text", nullable: true),
release_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true) release_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
@ -442,52 +490,52 @@ namespace Kyoo.Postgresql.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "metadata_id_season", name: "season_metadata_id",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), resource_id = table.Column<int>(type: "integer", nullable: false),
second_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), data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true) link = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_metadata_id_season", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_season_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey( table.ForeignKey(
name: "fk_metadata_id_season_providers_second_id", name: "fk_season_metadata_id_providers_provider_id",
column: x => x.second_id, column: x => x.provider_id,
principalTable: "providers", principalTable: "providers",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_metadata_id_season_seasons_first_id", name: "fk_season_metadata_id_seasons_season_id",
column: x => x.first_id, column: x => x.resource_id,
principalTable: "seasons", principalTable: "seasons",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "metadata_id_episode", name: "episode_metadata_id",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), resource_id = table.Column<int>(type: "integer", nullable: false),
second_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), data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true) link = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_metadata_id_episode", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_episode_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey( table.ForeignKey(
name: "fk_metadata_id_episode_episodes_first_id", name: "fk_episode_metadata_id_episodes_episode_id",
column: x => x.first_id, column: x => x.resource_id,
principalTable: "episodes", principalTable: "episodes",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_metadata_id_episode_providers_second_id", name: "fk_episode_metadata_id_providers_provider_id",
column: x => x.second_id, column: x => x.provider_id,
principalTable: "providers", principalTable: "providers",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -526,33 +574,43 @@ namespace Kyoo.Postgresql.Migrations
name: "watched_episodes", name: "watched_episodes",
columns: table => new columns: table => new
{ {
first_id = table.Column<int>(type: "integer", nullable: false), user_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false), episode_id = table.Column<int>(type: "integer", nullable: false),
watched_percentage = table.Column<int>(type: "integer", nullable: false) watched_percentage = table.Column<int>(type: "integer", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_watched_episodes", x => new { x.first_id, x.second_id }); table.PrimaryKey("pk_watched_episodes", x => new { x.user_id, x.episode_id });
table.ForeignKey( table.ForeignKey(
name: "fk_watched_episodes_episodes_second_id", name: "fk_watched_episodes_episodes_episode_id",
column: x => x.second_id, column: x => x.episode_id,
principalTable: "episodes", principalTable: "episodes",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_watched_episodes_users_first_id", name: "fk_watched_episodes_users_user_id",
column: x => x.first_id, column: x => x.user_id,
principalTable: "users", principalTable: "users",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "ix_collection_metadata_id_provider_id",
table: "collection_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_collections_slug", name: "ix_collections_slug",
table: "collections", table: "collections",
column: "slug", column: "slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "ix_episode_metadata_id_provider_id",
table: "episode_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_episodes_season_id", name: "ix_episodes_season_id",
table: "episodes", table: "episodes",
@ -583,54 +641,34 @@ namespace Kyoo.Postgresql.Migrations
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_link_collection_show_second_id", name: "ix_link_collection_show_show_id",
table: "link_collection_show", table: "link_collection_show",
column: "second_id"); column: "show_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_link_library_collection_second_id", name: "ix_link_library_collection_library_id",
table: "link_library_collection", table: "link_library_collection",
column: "second_id"); column: "library_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_link_library_provider_second_id", name: "ix_link_library_provider_provider_id",
table: "link_library_provider", table: "link_library_provider",
column: "second_id"); column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_link_library_show_second_id", name: "ix_link_library_show_show_id",
table: "link_library_show", table: "link_library_show",
column: "second_id"); column: "show_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_link_show_genre_second_id", name: "ix_link_show_genre_show_id",
table: "link_show_genre", table: "link_show_genre",
column: "second_id"); column: "show_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_link_user_show_second_id", name: "ix_link_user_show_watched_id",
table: "link_user_show", table: "link_user_show",
column: "second_id"); column: "watched_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_episode_second_id",
table: "metadata_id_episode",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_people_second_id",
table: "metadata_id_people",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_season_second_id",
table: "metadata_id_season",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_show_second_id",
table: "metadata_id_show",
column: "second_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_people_slug", name: "ix_people_slug",
@ -638,6 +676,11 @@ namespace Kyoo.Postgresql.Migrations
column: "slug", column: "slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "ix_people_metadata_id_provider_id",
table: "people_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_people_roles_people_id", name: "ix_people_roles_people_id",
table: "people_roles", table: "people_roles",
@ -654,6 +697,11 @@ namespace Kyoo.Postgresql.Migrations
column: "slug", column: "slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "ix_season_metadata_id_provider_id",
table: "season_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_seasons_show_id_season_number", name: "ix_seasons_show_id_season_number",
table: "seasons", table: "seasons",
@ -666,6 +714,11 @@ namespace Kyoo.Postgresql.Migrations
column: "slug", column: "slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "ix_show_metadata_id_provider_id",
table: "show_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_shows_slug", name: "ix_shows_slug",
table: "shows", table: "shows",
@ -677,6 +730,11 @@ namespace Kyoo.Postgresql.Migrations
table: "shows", table: "shows",
column: "studio_id"); column: "studio_id");
migrationBuilder.CreateIndex(
name: "ix_studio_metadata_id_provider_id",
table: "studio_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_studios_slug", name: "ix_studios_slug",
table: "studios", table: "studios",
@ -702,13 +760,19 @@ namespace Kyoo.Postgresql.Migrations
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_watched_episodes_second_id", name: "ix_watched_episodes_episode_id",
table: "watched_episodes", table: "watched_episodes",
column: "second_id"); column: "episode_id");
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable(
name: "collection_metadata_id");
migrationBuilder.DropTable(
name: "episode_metadata_id");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "link_collection_show"); name: "link_collection_show");
@ -728,20 +792,20 @@ namespace Kyoo.Postgresql.Migrations
name: "link_user_show"); name: "link_user_show");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "metadata_id_episode"); name: "people_metadata_id");
migrationBuilder.DropTable(
name: "metadata_id_people");
migrationBuilder.DropTable(
name: "metadata_id_season");
migrationBuilder.DropTable(
name: "metadata_id_show");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "people_roles"); name: "people_roles");
migrationBuilder.DropTable(
name: "season_metadata_id");
migrationBuilder.DropTable(
name: "show_metadata_id");
migrationBuilder.DropTable(
name: "studio_metadata_id");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "tracks"); name: "tracks");
@ -758,10 +822,10 @@ namespace Kyoo.Postgresql.Migrations
name: "genres"); name: "genres");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "providers"); name: "people");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "people"); name: "providers");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "episodes"); name: "episodes");

View File

@ -141,7 +141,7 @@ namespace Kyoo.Postgresql.Migrations
// language=PostgreSQL // language=PostgreSQL
migrationBuilder.Sql(@" migrationBuilder.Sql(@"
CREATE VIEW library_items AS CREATE VIEW library_items AS
SELECT s.id, s.slug, s.title, s.overview, s.status, s.start_air, s.end_air, s.poster, CASE SELECT s.id, s.slug, s.title, s.overview, s.status, s.start_air, s.end_air, s.images, CASE
WHEN s.is_movie THEN 'movie'::item_type WHEN s.is_movie THEN 'movie'::item_type
ELSE 'show'::item_type ELSE 'show'::item_type
END AS type END AS type
@ -149,11 +149,11 @@ namespace Kyoo.Postgresql.Migrations
WHERE NOT (EXISTS ( WHERE NOT (EXISTS (
SELECT 1 SELECT 1
FROM link_collection_show AS l FROM link_collection_show AS l
INNER JOIN collections AS c ON l.first_id = c.id INNER JOIN collections AS c ON l.collection_id = c.id
WHERE s.id = l.second_id)) WHERE s.id = l.show_id))
UNION ALL UNION ALL
SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status, SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status,
NULL AS start_air, NULL AS end_air, c0.poster, 'collection'::item_type AS type NULL AS start_air, NULL AS end_air, c0.images, 'collection'::item_type AS type
FROM collections AS c0"); FROM collections AS c0");
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Globalization;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using EFCore.NamingConventions.Internal;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql; using Npgsql;
@ -99,9 +101,55 @@ namespace Kyoo.Postgresql
.Property(x => x.ExtraData) .Property(x => x.ExtraData)
.HasColumnType("jsonb"); .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); base.OnModelCreating(modelBuilder);
} }
/// <inheritdoc />
protected override string MetadataName<T>()
{
SnakeCaseNameRewriter rewriter = new(CultureInfo.InvariantCulture);
return rewriter.RewriteName(typeof(T).Name + nameof(MetadataID));
}
/// <inheritdoc />
protected override string LinkName<T, T2>()
{
SnakeCaseNameRewriter rewriter = new(CultureInfo.InvariantCulture);
return rewriter.RewriteName("Link" + typeof(T).Name + typeof(T2).Name);
}
/// <inheritdoc />
protected override string LinkNameFk<T>()
{
SnakeCaseNameRewriter rewriter = new(CultureInfo.InvariantCulture);
return rewriter.RewriteName(typeof(T).Name + "ID");
}
/// <inheritdoc /> /// <inheritdoc />
protected override bool IsDuplicateException(Exception ex) protected override bool IsDuplicateException(Exception ex)
{ {

View File

@ -66,7 +66,7 @@ namespace Kyoo.Postgresql
x.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); x.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
if (_environment.IsDevelopment()) if (_environment.IsDevelopment())
x.EnableDetailedErrors().EnableSensitiveDataLogging(); x.EnableDetailedErrors().EnableSensitiveDataLogging();
}); }, ServiceLifetime.Transient);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -15,7 +15,7 @@ namespace Kyoo.SqLite.Migrations
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "TEXT", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "TEXT", nullable: true), Images = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "TEXT", nullable: true) Overview = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
@ -60,7 +60,7 @@ namespace Kyoo.SqLite.Migrations
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "TEXT", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "TEXT", nullable: true) Images = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -75,8 +75,7 @@ namespace Kyoo.SqLite.Migrations
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "TEXT", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Logo = table.Column<string>(type: "TEXT", nullable: true), Images = table.Column<string>(type: "TEXT", nullable: true)
LogoExtension = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -108,7 +107,8 @@ namespace Kyoo.SqLite.Migrations
Email = table.Column<string>(type: "TEXT", nullable: true), Email = table.Column<string>(type: "TEXT", nullable: true),
Password = table.Column<string>(type: "TEXT", nullable: true), Password = table.Column<string>(type: "TEXT", nullable: true),
Permissions = table.Column<string>(type: "TEXT", nullable: true), Permissions = table.Column<string>(type: "TEXT", nullable: true),
ExtraData = table.Column<string>(type: "TEXT", nullable: true) ExtraData = table.Column<string>(type: "TEXT", nullable: true),
Images = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -116,74 +116,100 @@ namespace Kyoo.SqLite.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Link<Library, Collection>", name: "LinkLibraryCollection",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), CollectionID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false) LibraryID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Link<Library, Collection>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_LinkLibraryCollection", x => new { x.CollectionID, x.LibraryID });
table.ForeignKey( table.ForeignKey(
name: "FK_Link<Library, Collection>_Collections_SecondID", name: "FK_LinkLibraryCollection_Collections_CollectionID",
column: x => x.SecondID, column: x => x.CollectionID,
principalTable: "Collections", principalTable: "Collections",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_Link<Library, Collection>_Libraries_FirstID", name: "FK_LinkLibraryCollection_Libraries_LibraryID",
column: x => x.FirstID, column: x => x.LibraryID,
principalTable: "Libraries", principalTable: "Libraries",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Link<Library, Provider>", name: "CollectionMetadataID",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), ResourceID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false) ProviderID = table.Column<int>(type: "INTEGER", nullable: false),
DataID = table.Column<string>(type: "TEXT", nullable: true),
Link = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Link<Library, Provider>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_CollectionMetadataID", x => new { x.ResourceID, x.ProviderID });
table.ForeignKey( table.ForeignKey(
name: "FK_Link<Library, Provider>_Libraries_FirstID", name: "FK_CollectionMetadataID_Collections_ResourceID",
column: x => x.FirstID, column: x => x.ResourceID,
principalTable: "Libraries", principalTable: "Collections",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_Link<Library, Provider>_Providers_SecondID", name: "FK_CollectionMetadataID_Providers_ProviderID",
column: x => x.SecondID, column: x => x.ProviderID,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MetadataID<People>", name: "LinkLibraryProvider",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), LibraryID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false), ProviderID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LinkLibraryProvider", x => new { x.LibraryID, x.ProviderID });
table.ForeignKey(
name: "FK_LinkLibraryProvider_Libraries_LibraryID",
column: x => x.LibraryID,
principalTable: "Libraries",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LinkLibraryProvider_Providers_ProviderID",
column: x => x.ProviderID,
principalTable: "Providers",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PeopleMetadataID",
columns: table => new
{
ResourceID = table.Column<int>(type: "INTEGER", nullable: false),
ProviderID = table.Column<int>(type: "INTEGER", nullable: false),
DataID = table.Column<string>(type: "TEXT", nullable: true), DataID = table.Column<string>(type: "TEXT", nullable: true),
Link = table.Column<string>(type: "TEXT", nullable: true) Link = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MetadataID<People>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_PeopleMetadataID", x => new { x.ResourceID, x.ProviderID });
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<People>_People_FirstID", name: "FK_PeopleMetadataID_People_ResourceID",
column: x => x.FirstID, column: x => x.ResourceID,
principalTable: "People", principalTable: "People",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<People>_Providers_SecondID", name: "FK_PeopleMetadataID_Providers_ProviderID",
column: x => x.SecondID, column: x => x.ProviderID,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -201,12 +227,9 @@ namespace Kyoo.SqLite.Migrations
Path = table.Column<string>(type: "TEXT", nullable: true), Path = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "TEXT", nullable: true), Overview = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<int>(type: "INTEGER", nullable: false), Status = table.Column<int>(type: "INTEGER", nullable: false),
TrailerUrl = table.Column<string>(type: "TEXT", nullable: true),
StartAir = table.Column<DateTime>(type: "TEXT", nullable: true), StartAir = table.Column<DateTime>(type: "TEXT", nullable: true),
EndAir = table.Column<DateTime>(type: "TEXT", nullable: true), EndAir = table.Column<DateTime>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "TEXT", nullable: true), Images = table.Column<string>(type: "TEXT", nullable: true),
Logo = table.Column<string>(type: "TEXT", nullable: true),
Backdrop = table.Column<string>(type: "TEXT", nullable: true),
IsMovie = table.Column<bool>(type: "INTEGER", nullable: false), IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
StudioID = table.Column<int>(type: "INTEGER", nullable: true) StudioID = table.Column<int>(type: "INTEGER", nullable: true)
}, },
@ -222,134 +245,133 @@ namespace Kyoo.SqLite.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Link<Collection, Show>", name: "StudioMetadataID",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), ResourceID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false) ProviderID = table.Column<int>(type: "INTEGER", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Link<Collection, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<Collection, Show>_Collections_FirstID",
column: x => x.FirstID,
principalTable: "Collections",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<Collection, Show>_Shows_SecondID",
column: x => x.SecondID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link<Library, Show>",
columns: table => new
{
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<Library, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<Library, Show>_Libraries_FirstID",
column: x => x.FirstID,
principalTable: "Libraries",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<Library, Show>_Shows_SecondID",
column: x => x.SecondID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link<Show, Genre>",
columns: table => new
{
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<Show, Genre>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<Show, Genre>_Genres_SecondID",
column: x => x.SecondID,
principalTable: "Genres",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<Show, Genre>_Shows_FirstID",
column: x => x.FirstID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link<User, Show>",
columns: table => new
{
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<User, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<User, Show>_Shows_SecondID",
column: x => x.SecondID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<User, Show>_Users_FirstID",
column: x => x.FirstID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MetadataID<Show>",
columns: table => new
{
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
DataID = table.Column<string>(type: "TEXT", nullable: true), DataID = table.Column<string>(type: "TEXT", nullable: true),
Link = table.Column<string>(type: "TEXT", nullable: true) Link = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MetadataID<Show>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_StudioMetadataID", x => new { x.ResourceID, x.ProviderID });
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<Show>_Providers_SecondID", name: "FK_StudioMetadataID_Providers_ProviderID",
column: x => x.SecondID, column: x => x.ProviderID,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<Show>_Shows_FirstID", name: "FK_StudioMetadataID_Studios_ResourceID",
column: x => x.FirstID, column: x => x.ResourceID,
principalTable: "Studios",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "LinkCollectionShow",
columns: table => new
{
CollectionID = table.Column<int>(type: "INTEGER", nullable: false),
ShowID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LinkCollectionShow", x => new { x.CollectionID, x.ShowID });
table.ForeignKey(
name: "FK_LinkCollectionShow_Collections_CollectionID",
column: x => x.CollectionID,
principalTable: "Collections",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LinkCollectionShow_Shows_ShowID",
column: x => x.ShowID,
principalTable: "Shows", principalTable: "Shows",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "LinkLibraryShow",
columns: table => new
{
LibraryID = table.Column<int>(type: "INTEGER", nullable: false),
ShowID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LinkLibraryShow", x => new { x.LibraryID, x.ShowID });
table.ForeignKey(
name: "FK_LinkLibraryShow_Libraries_LibraryID",
column: x => x.LibraryID,
principalTable: "Libraries",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LinkLibraryShow_Shows_ShowID",
column: x => x.ShowID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "LinkShowGenre",
columns: table => new
{
GenreID = table.Column<int>(type: "INTEGER", nullable: false),
ShowID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LinkShowGenre", x => new { x.GenreID, x.ShowID });
table.ForeignKey(
name: "FK_LinkShowGenre_Genres_GenreID",
column: x => x.GenreID,
principalTable: "Genres",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LinkShowGenre_Shows_ShowID",
column: x => x.ShowID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "LinkUserShow",
columns: table => new
{
UsersID = table.Column<int>(type: "INTEGER", nullable: false),
WatchedID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LinkUserShow", x => new { x.UsersID, x.WatchedID });
table.ForeignKey(
name: "FK_LinkUserShow_Shows_WatchedID",
column: x => x.WatchedID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LinkUserShow_Users_UsersID",
column: x => x.UsersID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PeopleRoles", name: "PeopleRoles",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "INTEGER", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
ForPeople = table.Column<bool>(type: "INTEGER", nullable: false),
PeopleID = table.Column<int>(type: "INTEGER", nullable: false), PeopleID = table.Column<int>(type: "INTEGER", nullable: false),
ShowID = table.Column<int>(type: "INTEGER", nullable: false), ShowID = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: true), Type = table.Column<string>(type: "TEXT", nullable: true),
@ -385,7 +407,7 @@ namespace Kyoo.SqLite.Migrations
Overview = table.Column<string>(type: "TEXT", nullable: true), Overview = table.Column<string>(type: "TEXT", nullable: true),
StartDate = table.Column<DateTime>(type: "TEXT", nullable: true), StartDate = table.Column<DateTime>(type: "TEXT", nullable: true),
EndDate = table.Column<DateTime>(type: "TEXT", nullable: true), EndDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "TEXT", nullable: true) Images = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -398,6 +420,32 @@ namespace Kyoo.SqLite.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "ShowMetadataID",
columns: table => new
{
ResourceID = table.Column<int>(type: "INTEGER", nullable: false),
ProviderID = table.Column<int>(type: "INTEGER", nullable: false),
DataID = table.Column<string>(type: "TEXT", nullable: true),
Link = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ShowMetadataID", x => new { x.ResourceID, x.ProviderID });
table.ForeignKey(
name: "FK_ShowMetadataID_Providers_ProviderID",
column: x => x.ProviderID,
principalTable: "Providers",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ShowMetadataID_Shows_ResourceID",
column: x => x.ResourceID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Episodes", name: "Episodes",
columns: table => new columns: table => new
@ -411,7 +459,7 @@ namespace Kyoo.SqLite.Migrations
EpisodeNumber = table.Column<int>(type: "INTEGER", nullable: true), EpisodeNumber = table.Column<int>(type: "INTEGER", nullable: true),
AbsoluteNumber = table.Column<int>(type: "INTEGER", nullable: true), AbsoluteNumber = table.Column<int>(type: "INTEGER", nullable: true),
Path = table.Column<string>(type: "TEXT", nullable: true), Path = table.Column<string>(type: "TEXT", nullable: true),
Thumb = table.Column<string>(type: "TEXT", nullable: true), Images = table.Column<string>(type: "TEXT", nullable: true),
Title = table.Column<string>(type: "TEXT", nullable: true), Title = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "TEXT", nullable: true), Overview = table.Column<string>(type: "TEXT", nullable: true),
ReleaseDate = table.Column<DateTime>(type: "TEXT", nullable: true) ReleaseDate = table.Column<DateTime>(type: "TEXT", nullable: true)
@ -434,52 +482,52 @@ namespace Kyoo.SqLite.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MetadataID<Season>", name: "SeasonMetadataID",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), ResourceID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false), ProviderID = table.Column<int>(type: "INTEGER", nullable: false),
DataID = table.Column<string>(type: "TEXT", nullable: true), DataID = table.Column<string>(type: "TEXT", nullable: true),
Link = table.Column<string>(type: "TEXT", nullable: true) Link = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MetadataID<Season>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_SeasonMetadataID", x => new { x.ResourceID, x.ProviderID });
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<Season>_Providers_SecondID", name: "FK_SeasonMetadataID_Providers_ProviderID",
column: x => x.SecondID, column: x => x.ProviderID,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<Season>_Seasons_FirstID", name: "FK_SeasonMetadataID_Seasons_ResourceID",
column: x => x.FirstID, column: x => x.ResourceID,
principalTable: "Seasons", principalTable: "Seasons",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MetadataID<Episode>", name: "EpisodeMetadataID",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), ResourceID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false), ProviderID = table.Column<int>(type: "INTEGER", nullable: false),
DataID = table.Column<string>(type: "TEXT", nullable: true), DataID = table.Column<string>(type: "TEXT", nullable: true),
Link = table.Column<string>(type: "TEXT", nullable: true) Link = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MetadataID<Episode>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_EpisodeMetadataID", x => new { x.ResourceID, x.ProviderID });
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<Episode>_Episodes_FirstID", name: "FK_EpisodeMetadataID_Episodes_ResourceID",
column: x => x.FirstID, column: x => x.ResourceID,
principalTable: "Episodes", principalTable: "Episodes",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataID<Episode>_Providers_SecondID", name: "FK_EpisodeMetadataID_Providers_ProviderID",
column: x => x.SecondID, column: x => x.ProviderID,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -518,33 +566,43 @@ namespace Kyoo.SqLite.Migrations
name: "WatchedEpisodes", name: "WatchedEpisodes",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "INTEGER", nullable: false), UserID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "INTEGER", nullable: false), EpisodeID = table.Column<int>(type: "INTEGER", nullable: false),
WatchedPercentage = table.Column<int>(type: "INTEGER", nullable: false) WatchedPercentage = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_WatchedEpisodes", x => new { x.UserID, x.EpisodeID });
table.ForeignKey( table.ForeignKey(
name: "FK_WatchedEpisodes_Episodes_SecondID", name: "FK_WatchedEpisodes_Episodes_EpisodeID",
column: x => x.SecondID, column: x => x.EpisodeID,
principalTable: "Episodes", principalTable: "Episodes",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_WatchedEpisodes_Users_FirstID", name: "FK_WatchedEpisodes_Users_UserID",
column: x => x.FirstID, column: x => x.UserID,
principalTable: "Users", principalTable: "Users",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_CollectionMetadataID_ProviderID",
table: "CollectionMetadataID",
column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Collections_Slug", name: "IX_Collections_Slug",
table: "Collections", table: "Collections",
column: "Slug", column: "Slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_EpisodeMetadataID_ProviderID",
table: "EpisodeMetadataID",
column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Episodes_SeasonID", name: "IX_Episodes_SeasonID",
table: "Episodes", table: "Episodes",
@ -575,54 +633,34 @@ namespace Kyoo.SqLite.Migrations
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<Collection, Show>_SecondID", name: "IX_LinkCollectionShow_ShowID",
table: "Link<Collection, Show>", table: "LinkCollectionShow",
column: "SecondID"); column: "ShowID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<Library, Collection>_SecondID", name: "IX_LinkLibraryCollection_LibraryID",
table: "Link<Library, Collection>", table: "LinkLibraryCollection",
column: "SecondID"); column: "LibraryID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<Library, Provider>_SecondID", name: "IX_LinkLibraryProvider_ProviderID",
table: "Link<Library, Provider>", table: "LinkLibraryProvider",
column: "SecondID"); column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<Library, Show>_SecondID", name: "IX_LinkLibraryShow_ShowID",
table: "Link<Library, Show>", table: "LinkLibraryShow",
column: "SecondID"); column: "ShowID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<Show, Genre>_SecondID", name: "IX_LinkShowGenre_ShowID",
table: "Link<Show, Genre>", table: "LinkShowGenre",
column: "SecondID"); column: "ShowID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<User, Show>_SecondID", name: "IX_LinkUserShow_WatchedID",
table: "Link<User, Show>", table: "LinkUserShow",
column: "SecondID"); column: "WatchedID");
migrationBuilder.CreateIndex(
name: "IX_MetadataID<Episode>_SecondID",
table: "MetadataID<Episode>",
column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_MetadataID<People>_SecondID",
table: "MetadataID<People>",
column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_MetadataID<Season>_SecondID",
table: "MetadataID<Season>",
column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_MetadataID<Show>_SecondID",
table: "MetadataID<Show>",
column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_People_Slug", name: "IX_People_Slug",
@ -630,6 +668,11 @@ namespace Kyoo.SqLite.Migrations
column: "Slug", column: "Slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_PeopleMetadataID_ProviderID",
table: "PeopleMetadataID",
column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_PeopleRoles_PeopleID", name: "IX_PeopleRoles_PeopleID",
table: "PeopleRoles", table: "PeopleRoles",
@ -646,6 +689,11 @@ namespace Kyoo.SqLite.Migrations
column: "Slug", column: "Slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_SeasonMetadataID_ProviderID",
table: "SeasonMetadataID",
column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Seasons_ShowID_SeasonNumber", name: "IX_Seasons_ShowID_SeasonNumber",
table: "Seasons", table: "Seasons",
@ -658,6 +706,11 @@ namespace Kyoo.SqLite.Migrations
column: "Slug", column: "Slug",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_ShowMetadataID_ProviderID",
table: "ShowMetadataID",
column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Shows_Slug", name: "IX_Shows_Slug",
table: "Shows", table: "Shows",
@ -669,6 +722,11 @@ namespace Kyoo.SqLite.Migrations
table: "Shows", table: "Shows",
column: "StudioID"); column: "StudioID");
migrationBuilder.CreateIndex(
name: "IX_StudioMetadataID_ProviderID",
table: "StudioMetadataID",
column: "ProviderID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Studios_Slug", name: "IX_Studios_Slug",
table: "Studios", table: "Studios",
@ -694,46 +752,52 @@ namespace Kyoo.SqLite.Migrations
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_WatchedEpisodes_SecondID", name: "IX_WatchedEpisodes_EpisodeID",
table: "WatchedEpisodes", table: "WatchedEpisodes",
column: "SecondID"); column: "EpisodeID");
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Collection, Show>"); name: "CollectionMetadataID");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Library, Collection>"); name: "EpisodeMetadataID");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Library, Provider>"); name: "LinkCollectionShow");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Library, Show>"); name: "LinkLibraryCollection");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Show, Genre>"); name: "LinkLibraryProvider");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<User, Show>"); name: "LinkLibraryShow");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MetadataID<Episode>"); name: "LinkShowGenre");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MetadataID<People>"); name: "LinkUserShow");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MetadataID<Season>"); name: "PeopleMetadataID");
migrationBuilder.DropTable(
name: "MetadataID<Show>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PeopleRoles"); name: "PeopleRoles");
migrationBuilder.DropTable(
name: "SeasonMetadataID");
migrationBuilder.DropTable(
name: "ShowMetadataID");
migrationBuilder.DropTable(
name: "StudioMetadataID");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Tracks"); name: "Tracks");
@ -750,10 +814,10 @@ namespace Kyoo.SqLite.Migrations
name: "Genres"); name: "Genres");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Providers"); name: "People");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "People"); name: "Providers");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Episodes"); name: "Episodes");

View File

@ -154,19 +154,19 @@ namespace Kyoo.SqLite.Migrations
// language=SQLite // language=SQLite
migrationBuilder.Sql(@" migrationBuilder.Sql(@"
CREATE VIEW LibraryItems AS CREATE VIEW LibraryItems AS
SELECT s.ID, s.Slug, s.Title, s.Overview, s.Status, s.StartAir, s.EndAir, s.Poster, CASE SELECT s.ID, s.Slug, s.Title, s.Overview, s.Status, s.StartAir, s.EndAir, s.Images, CASE
WHEN s.IsMovie THEN 1 WHEN s.IsMovie THEN 1
ELSE 0 ELSE 0
END AS Type END AS Type
FROM Shows AS s FROM Shows AS s
WHERE NOT (EXISTS ( WHERE NOT (EXISTS (
SELECT 1 SELECT 1
FROM 'Link<Collection, Show>' AS l FROM LinkCollectionShow AS l
INNER JOIN Collections AS c ON l.FirstID = c.ID INNER JOIN Collections AS c ON l.CollectionID = c.ID
WHERE s.ID = l.SecondID)) WHERE s.ID = l.ShowID))
UNION ALL UNION ALL
SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 3 AS Status, SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 0 AS Status,
NULL AS StartAir, NULL AS EndAir, c0.Poster, 2 AS Type NULL AS StartAir, NULL AS EndAir, c0.Images, 2 AS Type
FROM collections AS c0"); FROM collections AS c0");
} }

File diff suppressed because it is too large Load Diff

View File

@ -102,12 +102,41 @@ namespace Kyoo.SqLite
.Property(x => x.Type) .Property(x => x.Type)
.HasConversion<int>(); .HasConversion<int>();
ValueConverter<Dictionary<string, string>, string> jsonConvertor = new( ValueConverter<Dictionary<string, string>, string> extraDataConvertor = new(
x => JsonConvert.SerializeObject(x), x => JsonConvert.SerializeObject(x),
x => JsonConvert.DeserializeObject<Dictionary<string, string>>(x)); x => JsonConvert.DeserializeObject<Dictionary<string, string>>(x));
modelBuilder.Entity<User>() modelBuilder.Entity<User>()
.Property(x => x.ExtraData) .Property(x => x.ExtraData)
.HasConversion(extraDataConvertor);
ValueConverter<Dictionary<int, string>, string> jsonConvertor = new(
x => JsonConvert.SerializeObject(x),
x => JsonConvert.DeserializeObject<Dictionary<int, string>>(x));
modelBuilder.Entity<LibraryItem>()
.Property(x => x.Images)
.HasConversion(jsonConvertor); .HasConversion(jsonConvertor);
modelBuilder.Entity<Collection>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<Show>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<Season>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<Episode>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<People>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<Provider>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<User>()
.Property(x => x.Images)
.HasConversion(jsonConvertor);
modelBuilder.Entity<LibraryItem>() modelBuilder.Entity<LibraryItem>()
.ToView("LibraryItems") .ToView("LibraryItems")
@ -115,6 +144,24 @@ namespace Kyoo.SqLite
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
} }
/// <inheritdoc />
protected override string MetadataName<T>()
{
return typeof(T).Name + nameof(MetadataID);
}
/// <inheritdoc />
protected override string LinkName<T, T2>()
{
return "Link" + typeof(T).Name + typeof(T2).Name;
}
/// <inheritdoc />
protected override string LinkNameFk<T>()
{
return typeof(T).Name + "ID";
}
/// <inheritdoc /> /// <inheritdoc />
protected override bool IsDuplicateException(Exception ex) protected override bool IsDuplicateException(Exception ex)
{ {

View File

@ -66,7 +66,7 @@ namespace Kyoo.SqLite
x.UseSqlite(_configuration.GetDatabaseConnection("sqlite")); x.UseSqlite(_configuration.GetDatabaseConnection("sqlite"));
if (_environment.IsDevelopment()) if (_environment.IsDevelopment())
x.EnableDetailedErrors().EnableSensitiveDataLogging(); x.EnableDetailedErrors().EnableSensitiveDataLogging();
}); }, ServiceLifetime.Transient);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -1,67 +0,0 @@
using System;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Xunit.Abstractions;
namespace Kyoo.Tests
{
public class RepositoryActivator : IDisposable, IAsyncDisposable
{
public TestContext Context { get; }
public ILibraryManager LibraryManager { get; }
private readonly DatabaseContext _database;
public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null)
{
Context = postgres == null
? new SqLiteTestContext(output)
: new PostgresTestContext(postgres, output);
_database = Context.New();
ProviderRepository provider = new(_database);
LibraryRepository library = new(_database, provider);
CollectionRepository collection = new(_database);
GenreRepository genre = new(_database);
StudioRepository studio = new(_database);
PeopleRepository people = new(_database, provider,
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository));
ShowRepository show = new(_database, studio, people, genre, provider);
SeasonRepository season = new(_database, provider);
LibraryItemRepository libraryItem = new(_database,
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
TrackRepository track = new(_database);
EpisodeRepository episode = new(_database, provider, track);
UserRepository user = new(_database);
LibraryManager = new LibraryManager(new IBaseRepository[] {
provider,
library,
libraryItem,
collection,
show,
season,
episode,
track,
people,
studio,
genre,
user
});
}
public void Dispose()
{
_database.Dispose();
Context.Dispose();
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
await _database.DisposeAsync();
await Context.DisposeAsync();
}
}
}

View File

@ -1,37 +0,0 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class CollectionTests : ACollectionTests
{
public CollectionTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class CollectionTests : ACollectionTests
{
public CollectionTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ACollectionTests : RepositoryTests<Collection>
{
private readonly ICollectionRepository _repository;
protected ACollectionTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.CollectionRepository;
}
}
}

View File

@ -1,51 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class LibraryTests : ALibraryTests
{
public LibraryTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class LibraryTests : ALibraryTests
{
public LibraryTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ALibraryTests : RepositoryTests<Library>
{
private readonly ILibraryRepository _repository;
protected ALibraryTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.LibraryRepository;
}
[Fact]
public async Task CreateWithProvider()
{
Library library = TestSample.GetNew<Library>();
library.Providers = new[] { TestSample.Get<Provider>() };
await _repository.Create(library);
Library retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.Providers);
Assert.Equal(1, retrieved.Providers.Count);
Assert.Equal(TestSample.Get<Provider>().Slug, retrieved.Providers.First().Slug);
}
}
}

View File

@ -1,37 +0,0 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class PeopleTests : APeopleTests
{
public PeopleTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class PeopleTests : APeopleTests
{
public PeopleTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class APeopleTests : RepositoryTests<People>
{
private readonly IPeopleRepository _repository;
protected APeopleTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.PeopleRepository;
}
}
}

View File

@ -1,79 +0,0 @@
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class SeasonTests : ASeasonTests
{
public SeasonTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class SeasonTests : ASeasonTests
{
public SeasonTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ASeasonTests : RepositoryTests<Season>
{
private readonly ISeasonRepository _repository;
protected ASeasonTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.SeasonRepository;
}
[Fact]
public async Task SlugEditTest()
{
Season season = await _repository.Get(1);
Assert.Equal("anohana-s1", season.Slug);
Show show = new()
{
ID = season.ShowID,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
season = await _repository.Get(1);
Assert.Equal("new-slug-s1", season.Slug);
}
[Fact]
public async Task SeasonNumberEditTest()
{
Season season = await _repository.Get(1);
Assert.Equal("anohana-s1", season.Slug);
await _repository.Edit(new Season
{
ID = 1,
SeasonNumber = 2
}, false);
season = await _repository.Get(1);
Assert.Equal("anohana-s2", season.Slug);
}
[Fact]
public async Task SeasonCreationSlugTest()
{
Season season = await _repository.Create(new Season
{
ShowID = TestSample.Get<Show>().ID,
SeasonNumber = 2
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
}
}
}

View File

@ -1,212 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Kyoo.Models;
using Kyoo.Models.Attributes;
using Xunit;
namespace Kyoo.Tests.Utility
{
public class MergerTests
{
[Fact]
public void NullifyTest()
{
Genre genre = new("test")
{
ID = 5
};
Merger.Nullify(genre);
Assert.Equal(0, genre.ID);
Assert.Null(genre.Name);
Assert.Null(genre.Slug);
}
[Fact]
public void MergeTest()
{
Genre genre = new()
{
ID = 5
};
Genre genre2 = new()
{
Name = "test"
};
Genre ret = Merger.Merge(genre, genre2);
Assert.True(ReferenceEquals(genre, ret));
Assert.Equal(5, ret.ID);
Assert.Equal("test", genre.Name);
Assert.Null(genre.Slug);
}
[Fact]
[SuppressMessage("ReSharper", "ExpressionIsAlwaysNull")]
public void MergeNullTests()
{
Genre genre = new()
{
ID = 5
};
Assert.True(ReferenceEquals(genre, Merger.Merge(genre, null)));
Assert.True(ReferenceEquals(genre, Merger.Merge(null, genre)));
Assert.Null(Merger.Merge<Genre>(null, null));
}
private class TestIOnMerge : IOnMerge
{
public void OnMerge(object other)
{
Exception exception = new();
exception.Data[0] = other;
throw exception;
}
}
[Fact]
public void OnMergeTest()
{
TestIOnMerge test = new();
TestIOnMerge test2 = new();
Assert.Throws<Exception>(() => Merger.Merge(test, test2));
try
{
Merger.Merge(test, test2);
}
catch (Exception ex)
{
Assert.True(ReferenceEquals(test2, ex.Data[0]));
}
}
private class Test
{
public int ID { get; set; }
public int[] Numbers { get; set; }
}
[Fact]
public void GlobalMergeListTest()
{
Test test = new()
{
ID = 5,
Numbers = new [] { 1 }
};
Test test2 = new()
{
Numbers = new [] { 3 }
};
Test ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Numbers.Length);
Assert.Equal(1, ret.Numbers[0]);
Assert.Equal(3, ret.Numbers[1]);
}
[Fact]
public void GlobalMergeListDuplicatesTest()
{
Test test = new()
{
ID = 5,
Numbers = new [] { 1 }
};
Test test2 = new()
{
Numbers = new []
{
1,
3,
3
}
};
Test ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(4, ret.Numbers.Length);
Assert.Equal(1, ret.Numbers[0]);
Assert.Equal(1, ret.Numbers[1]);
Assert.Equal(3, ret.Numbers[2]);
Assert.Equal(3, ret.Numbers[3]);
}
[Fact]
public void GlobalMergeListDuplicatesResourcesTest()
{
Show test = new()
{
ID = 5,
Genres = new [] { new Genre("test") }
};
Show test2 = new()
{
Genres = new []
{
new Genre("test"),
new Genre("test2")
}
};
Show ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Genres.Count);
Assert.Equal("test", ret.Genres.ToArray()[0].Slug);
Assert.Equal("test2", ret.Genres.ToArray()[1].Slug);
}
[Fact]
public void MergeListTest()
{
int[] first = { 1 };
int[] second = {
3,
3
};
int[] ret = Merger.MergeLists(first, second);
Assert.Equal(3, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(3, ret[1]);
Assert.Equal(3, ret[2]);
}
[Fact]
public void MergeListDuplicateTest()
{
int[] first = { 1 };
int[] second = {
1,
3,
3
};
int[] ret = Merger.MergeLists(first, second);
Assert.Equal(4, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(1, ret[1]);
Assert.Equal(3, ret[2]);
Assert.Equal(3, ret[3]);
}
[Fact]
public void MergeListDuplicateCustomEqualityTest()
{
int[] first = { 1 };
int[] second = {
3,
2
};
int[] ret = Merger.MergeLists(first, second, (x, y) => x % 2 == y % 2);
Assert.Equal(2, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(2, ret[1]);
}
}
}

View File

@ -0,0 +1,79 @@
using System.Collections.Generic;
using Kyoo.Models;
using TMDbLib.Objects.Search;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="SearchCollection"/> into a <see cref="Collection"/>.
/// </summary>
/// <param name="collection">The collection to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted collection as a <see cref="Collection"/>.</returns>
public static Collection ToCollection(this TMDbLib.Objects.Collections.Collection collection, Provider provider)
{
return new Collection
{
Slug = Utility.ToSlug(collection.Name),
Name = collection.Name,
Overview = collection.Overview,
Images = new Dictionary<int, string>
{
[Images.Poster] = collection.PosterPath != null
? $"https://image.tmdb.org/t/p/original{collection.PosterPath}"
: null,
[Images.Thumbnail] = collection.BackdropPath != null
? $"https://image.tmdb.org/t/p/original{collection.BackdropPath}"
: null
},
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/collection/{collection.Id}",
DataID = collection.Id.ToString()
}
}
};
}
/// <summary>
/// Convert a <see cref="SearchCollection"/> into a <see cref="Collection"/>.
/// </summary>
/// <param name="collection">The collection to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted collection as a <see cref="Collection"/>.</returns>
public static Collection ToCollection(this SearchCollection collection, Provider provider)
{
return new Collection
{
Slug = Utility.ToSlug(collection.Name),
Name = collection.Name,
Images = new Dictionary<int, string>
{
[Images.Poster] = collection.PosterPath != null
? $"https://image.tmdb.org/t/p/original{collection.PosterPath}"
: null,
[Images.Thumbnail] = collection.BackdropPath != null
? $"https://image.tmdb.org/t/p/original{collection.BackdropPath}"
: null
},
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/collection/{collection.Id}",
DataID = collection.Id.ToString()
}
}
};
}
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using Kyoo.Models;
using TMDbLib.Objects.TvShows;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="TvEpisode"/> into a <see cref="Episode"/>.
/// </summary>
/// <param name="episode">The episode to convert.</param>
/// <param name="showID">The ID of the show inside TheMovieDb.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted episode as a <see cref="Episode"/>.</returns>
public static Episode ToEpisode(this TvEpisode episode, int showID, Provider provider)
{
return new Episode
{
SeasonNumber = episode.SeasonNumber,
EpisodeNumber = episode.EpisodeNumber,
Title = episode.Name,
Overview = episode.Overview,
ReleaseDate = episode.AirDate,
Images = new Dictionary<int, string>
{
[Images.Thumbnail] = episode.StillPath != null
? $"https://image.tmdb.org/t/p/original{episode.StillPath}"
: null
},
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/tv/{showID}" +
$"/season/{episode.SeasonNumber}/episode/{episode.EpisodeNumber}",
DataID = episode.Id.ToString()
}
}
};
}
}
}

View File

@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models;
using TMDbLib.Objects.Movies;
using TMDbLib.Objects.Search;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="Movie"/> into a <see cref="Show"/>.
/// </summary>
/// <param name="movie">The movie to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted movie as a <see cref="Show"/>.</returns>
public static Show ToShow(this Movie movie, Provider provider)
{
return new Show
{
Slug = Utility.ToSlug(movie.Title),
Title = movie.Title,
Aliases = movie.AlternativeTitles.Titles.Select(x => x.Title).ToArray(),
Overview = movie.Overview,
Status = movie.Status == "Released" ? Status.Finished : Status.Planned,
StartAir = movie.ReleaseDate,
EndAir = movie.ReleaseDate,
Images = new Dictionary<int, string>
{
[Images.Poster] = movie.PosterPath != null
? $"https://image.tmdb.org/t/p/original{movie.PosterPath}"
: null,
[Images.Thumbnail] = movie.BackdropPath != null
? $"https://image.tmdb.org/t/p/original{movie.BackdropPath}"
: null,
[Images.Trailer] = movie.Videos?.Results
.Where(x => x.Type is "Trailer" or "Teaser" && x.Site == "YouTube")
.Select(x => "https://www.youtube.com/watch?v=" + x.Key).FirstOrDefault(),
},
Genres = movie.Genres.Select(x => new Genre(x.Name)).ToArray(),
Studio = !string.IsNullOrEmpty(movie.ProductionCompanies.FirstOrDefault()?.Name)
? new Studio(movie.ProductionCompanies.First().Name)
: null,
IsMovie = true,
People = movie.Credits.Cast
.Select(x => x.ToPeople(provider))
.Concat(movie.Credits.Crew.Select(x => x.ToPeople(provider)))
.ToArray(),
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/movie/{movie.Id}",
DataID = movie.Id.ToString()
}
}
};
}
/// <summary>
/// Convert a <see cref="SearchMovie"/> into a <see cref="Show"/>.
/// </summary>
/// <param name="movie">The movie to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted movie as a <see cref="Show"/>.</returns>
public static Show ToShow(this SearchMovie movie, Provider provider)
{
return new Show
{
Slug = Utility.ToSlug(movie.Title),
Title = movie.Title,
Overview = movie.Overview,
StartAir = movie.ReleaseDate,
EndAir = movie.ReleaseDate,
Images = new Dictionary<int, string>
{
[Images.Poster] = movie.PosterPath != null
? $"https://image.tmdb.org/t/p/original{movie.PosterPath}"
: null,
[Images.Thumbnail] = movie.BackdropPath != null
? $"https://image.tmdb.org/t/p/original{movie.BackdropPath}"
: null,
},
IsMovie = true,
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/movie/{movie.Id}",
DataID = movie.Id.ToString()
}
}
};
}
}
}

View File

@ -0,0 +1,182 @@
using System.Collections.Generic;
using Kyoo.Models;
using TMDbLib.Objects.General;
using TMDbLib.Objects.People;
using TMDbLib.Objects.Search;
using Images = Kyoo.Models.Images;
using TvCast = TMDbLib.Objects.TvShows.Cast;
using MovieCast = TMDbLib.Objects.Movies.Cast;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="MovieCast"/> to a <see cref="PeopleRole"/>.
/// </summary>
/// <param name="cast">An internal TheMovieDB cast.</param>
/// <param name="provider">The provider that represent TheMovieDB inside Kyoo.</param>
/// <returns>A <see cref="PeopleRole"/> representing the movie cast.</returns>
public static PeopleRole ToPeople(this MovieCast cast, Provider provider)
{
return new PeopleRole
{
People = new People
{
Slug = Utility.ToSlug(cast.Name),
Name = cast.Name,
Images = new Dictionary<int, string>
{
[Images.Poster] = cast.ProfilePath != null
? $"https://image.tmdb.org/t/p/original{cast.ProfilePath}"
: null
},
ExternalIDs = new[]
{
new MetadataID
{
Provider = provider,
DataID = cast.Id.ToString(),
Link = $"https://www.themoviedb.org/person/{cast.Id}"
}
}
},
Type = "Actor",
Role = cast.Character
};
}
/// <summary>
/// Convert a <see cref="TvCast"/> to a <see cref="PeopleRole"/>.
/// </summary>
/// <param name="cast">An internal TheMovieDB cast.</param>
/// <param name="provider">The provider that represent TheMovieDB inside Kyoo.</param>
/// <returns>A <see cref="PeopleRole"/> representing the movie cast.</returns>
public static PeopleRole ToPeople(this TvCast cast, Provider provider)
{
return new PeopleRole
{
People = new People
{
Slug = Utility.ToSlug(cast.Name),
Name = cast.Name,
Images = new Dictionary<int, string>
{
[Images.Poster] = cast.ProfilePath != null
? $"https://image.tmdb.org/t/p/original{cast.ProfilePath}"
: null
},
ExternalIDs = new[]
{
new MetadataID
{
Provider = provider,
DataID = cast.Id.ToString(),
Link = $"https://www.themoviedb.org/person/{cast.Id}"
}
}
},
Type = "Actor",
Role = cast.Character
};
}
/// <summary>
/// Convert a <see cref="Crew"/> to a <see cref="PeopleRole"/>.
/// </summary>
/// <param name="crew">An internal TheMovieDB crew member.</param>
/// <param name="provider">The provider that represent TheMovieDB inside Kyoo.</param>
/// <returns>A <see cref="PeopleRole"/> representing the movie crew.</returns>
public static PeopleRole ToPeople(this Crew crew, Provider provider)
{
return new PeopleRole
{
People = new People
{
Slug = Utility.ToSlug(crew.Name),
Name = crew.Name,
Images = new Dictionary<int, string>
{
[Images.Poster] = crew.ProfilePath != null
? $"https://image.tmdb.org/t/p/original{crew.ProfilePath}"
: null
},
ExternalIDs = new[]
{
new MetadataID
{
Provider = provider,
DataID = crew.Id.ToString(),
Link = $"https://www.themoviedb.org/person/{crew.Id}"
}
}
},
Type = crew.Department,
Role = crew.Job
};
}
/// <summary>
/// Convert a <see cref="Person"/> to a <see cref="People"/>.
/// </summary>
/// <param name="person">An internal TheMovieDB person.</param>
/// <param name="provider">The provider that represent TheMovieDB inside Kyoo.</param>
/// <returns>A <see cref="People"/> representing the person.</returns>
public static People ToPeople(this Person person, Provider provider)
{
return new People
{
Slug = Utility.ToSlug(person.Name),
Name = person.Name,
Images = new Dictionary<int, string>
{
[Images.Poster] = person.ProfilePath != null
? $"https://image.tmdb.org/t/p/original{person.ProfilePath}"
: null
},
ExternalIDs = new[]
{
new MetadataID
{
Provider = provider,
DataID = person.Id.ToString(),
Link = $"https://www.themoviedb.org/person/{person.Id}"
}
}
};
}
/// <summary>
/// Convert a <see cref="SearchPerson"/> to a <see cref="People"/>.
/// </summary>
/// <param name="person">An internal TheMovieDB person.</param>
/// <param name="provider">The provider that represent TheMovieDB inside Kyoo.</param>
/// <returns>A <see cref="People"/> representing the person.</returns>
public static People ToPeople(this SearchPerson person, Provider provider)
{
return new People
{
Slug = Utility.ToSlug(person.Name),
Name = person.Name,
Images = new Dictionary<int, string>
{
[Images.Poster] = person.ProfilePath != null
? $"https://image.tmdb.org/t/p/original{person.ProfilePath}"
: null
},
ExternalIDs = new[]
{
new MetadataID
{
Provider = provider,
DataID = person.Id.ToString(),
Link = $"https://www.themoviedb.org/person/{person.Id}"
}
}
};
}
}
}

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using Kyoo.Models;
using TMDbLib.Objects.TvShows;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="TvSeason"/> into a <see cref="Season"/>.
/// </summary>
/// <param name="season">The season to convert.</param>
/// <param name="showID">The ID of the show inside TheMovieDb.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted season as a <see cref="Season"/>.</returns>
public static Season ToSeason(this TvSeason season, int showID, Provider provider)
{
return new Season
{
SeasonNumber = season.SeasonNumber,
Title = season.Name,
Overview = season.Overview,
StartDate = season.AirDate,
Images = new Dictionary<int, string>
{
[Images.Poster] = season.PosterPath != null
? $"https://image.tmdb.org/t/p/original{season.PosterPath}"
: null
},
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/tv/{showID}/season/{season.SeasonNumber}",
DataID = season.Id?.ToString()
}
}
};
}
}
}

View File

@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models;
using TMDbLib.Objects.Search;
using TMDbLib.Objects.TvShows;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="TvShow"/> to a <see cref="Show"/>.
/// </summary>
/// <param name="tv">The show to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>A converted <see cref="TvShow"/> as a <see cref="Show"/>.</returns>
public static Show ToShow(this TvShow tv, Provider provider)
{
return new Show
{
Slug = Utility.ToSlug(tv.Name),
Title = tv.Name,
Aliases = tv.AlternativeTitles.Results.Select(x => x.Title).ToArray(),
Overview = tv.Overview,
Status = tv.Status == "Ended" ? Status.Finished : Status.Planned,
StartAir = tv.FirstAirDate,
EndAir = tv.LastAirDate,
Images = new Dictionary<int, string>
{
[Images.Poster] = tv.PosterPath != null
? $"https://image.tmdb.org/t/p/original{tv.PosterPath}"
: null,
[Images.Thumbnail] = tv.BackdropPath != null
? $"https://image.tmdb.org/t/p/original{tv.BackdropPath}"
: null,
[Images.Trailer] = tv.Videos?.Results
.Where(x => x.Type is "Trailer" or "Teaser" && x.Site == "YouTube")
.Select(x => "https://www.youtube.com/watch?v=" + x.Key).FirstOrDefault()
},
Genres = tv.Genres.Select(x => new Genre(x.Name)).ToArray(),
Studio = !string.IsNullOrEmpty(tv.ProductionCompanies.FirstOrDefault()?.Name)
? new Studio(tv.ProductionCompanies.First().Name)
: null,
People = tv.Credits.Cast
.Select(x => x.ToPeople(provider))
.Concat(tv.Credits.Crew.Select(x => x.ToPeople(provider)))
.ToArray(),
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/tv/{tv.Id}",
DataID = tv.Id.ToString()
}
}
};
}
/// <summary>
/// Convert a <see cref="SearchTv"/> to a <see cref="Show"/>.
/// </summary>
/// <param name="tv">The show to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>A converted <see cref="SearchTv"/> as a <see cref="Show"/>.</returns>
public static Show ToShow(this SearchTv tv, Provider provider)
{
return new Show
{
Slug = Utility.ToSlug(tv.Name),
Title = tv.Name,
Overview = tv.Overview,
StartAir = tv.FirstAirDate,
Images = new Dictionary<int, string>
{
[Images.Poster] = tv.PosterPath != null
? $"https://image.tmdb.org/t/p/original{tv.PosterPath}"
: null,
[Images.Thumbnail] = tv.BackdropPath != null
? $"https://image.tmdb.org/t/p/original{tv.BackdropPath}"
: null,
},
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/tv/{tv.Id}",
DataID = tv.Id.ToString()
}
}
};
}
}
}

View File

@ -0,0 +1,60 @@
using Kyoo.Models;
using TMDbLib.Objects.Companies;
using TMDbLib.Objects.Search;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A class containing extensions methods to convert from TMDB's types to Kyoo's types.
/// </summary>
public static partial class Convertors
{
/// <summary>
/// Convert a <see cref="Company"/> into a <see cref="Studio"/>.
/// </summary>
/// <param name="company">The company to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted company as a <see cref="Studio"/>.</returns>
public static Studio ToStudio(this Company company, Provider provider)
{
return new Studio
{
Slug = Utility.ToSlug(company.Name),
Name = company.Name,
ExternalIDs = new []
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/company/{company.Id}",
DataID = company.Id.ToString()
}
}
};
}
/// <summary>
/// Convert a <see cref="SearchCompany"/> into a <see cref="Studio"/>.
/// </summary>
/// <param name="company">The company to convert.</param>
/// <param name="provider">The provider representing TheMovieDb.</param>
/// <returns>The converted company as a <see cref="Studio"/>.</returns>
public static Studio ToStudio(this SearchCompany company, Provider provider)
{
return new Studio
{
Slug = Utility.ToSlug(company.Name),
Name = company.Name,
ExternalIDs = new[]
{
new MetadataID
{
Provider = provider,
Link = $"https://www.themoviedb.org/company/{company.Id}",
DataID = company.Id.ToString()
}
}
};
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
<RootNamespace>Kyoo.TheMovieDb</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/the-moviedb</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDependencyFile>false</GenerateDependencyFile>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0.0" />
<PackageReference Include="TMDbLib" Version="1.8.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using Autofac;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
using Kyoo.TheMovieDb.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A plugin that add a <see cref="IMetadataProvider"/> for TheMovieDB.
/// </summary>
public class PluginTmdb : IPlugin
{
/// <inheritdoc />
public string Slug => "the-moviedb";
/// <inheritdoc />
public string Name => "TheMovieDb Provider";
/// <inheritdoc />
public string Description => "A metadata provider for TheMovieDB.";
/// <inheritdoc />
public ICollection<Type> Provides => new []
{
typeof(IMetadataProvider)
};
/// <inheritdoc />
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
/// <inheritdoc />
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
/// <summary>
/// The configuration to use.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The configuration manager used to register typed/untyped implementations.
/// </summary>
[Injected] public IConfigurationManager ConfigurationManager { private get; set; }
/// <summary>
/// Create a new tmdb module instance and use the given configuration.
/// </summary>
/// <param name="configuration">The configuration to use</param>
public PluginTmdb(IConfiguration configuration)
{
_configuration = configuration;
}
/// <inheritdoc />
public void Configure(ContainerBuilder builder)
{
builder.RegisterProvider<TheMovieDbProvider>();
}
/// <inheritdoc />
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
{
services.Configure<TheMovieDbOptions>(_configuration.GetSection(TheMovieDbOptions.Path));
}
/// <inheritdoc />
public void ConfigureAspNet(IApplicationBuilder app)
{
ConfigurationManager.AddTyped<TheMovieDbOptions>(TheMovieDbOptions.Path);
}
}
}

View File

@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.TheMovieDb.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TMDbLib.Client;
using TMDbLib.Objects.Movies;
using TMDbLib.Objects.Search;
using TMDbLib.Objects.TvShows;
namespace Kyoo.TheMovieDb
{
/// <summary>
/// A metadata provider for TheMovieDb.
/// </summary>
public class TheMovieDbProvider : IMetadataProvider
{
/// <summary>
/// The API key used to authenticate with TheMovieDb API.
/// </summary>
private readonly IOptions<TheMovieDbOptions> _apiKey;
/// <summary>
/// The logger to use in ase of issue.
/// </summary>
private readonly ILogger<TheMovieDbProvider> _logger;
/// <inheritdoc />
public Provider Provider => new()
{
Slug = "the-moviedb",
Name = "TheMovieDB",
Images = new Dictionary<int, string>
{
[Images.Logo] = "https://www.themoviedb.org/assets/2/v4/logos/v2/" +
"blue_short-8e7b30f73a4020692ccca9c88bafe5dcb6f8a62a4c6bc55cd9ba82bb2cd95f6c.svg"
}
};
/// <summary>
/// Create a new <see cref="TheMovieDbProvider"/> using the given api key.
/// </summary>
/// <param name="apiKey">The api key</param>
/// <param name="logger">The logger to use in case of issue.</param>
public TheMovieDbProvider(IOptions<TheMovieDbOptions> apiKey, ILogger<TheMovieDbProvider> logger)
{
_apiKey = apiKey;
_logger = logger;
}
/// <inheritdoc />
public Task<T> Get<T>(T item)
where T : class, IResource
{
return item switch
{
Collection collection => _GetCollection(collection) as Task<T>,
Show show => _GetShow(show) as Task<T>,
Season season => _GetSeason(season) as Task<T>,
Episode episode => _GetEpisode(episode) as Task<T>,
People person => _GetPerson(person) as Task<T>,
Studio studio => _GetStudio(studio) as Task<T>,
_ => null
};
}
/// <summary>
/// Get a collection using it's id, if the id is not present in the collection, fallback to a name search.
/// </summary>
/// <param name="collection">The collection to search for</param>
/// <returns>A collection containing metadata from TheMovieDb</returns>
private async Task<Collection> _GetCollection(Collection collection)
{
if (!collection.TryGetID(Provider.Slug, out int id))
{
Collection found = (await _SearchCollections(collection.Name ?? collection.Slug)).FirstOrDefault();
if (found?.TryGetID(Provider.Slug, out id) != true)
return found;
}
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.GetCollectionAsync(id)).ToCollection(Provider);
}
/// <summary>
/// Get a show using it's id, if the id is not present in the show, fallback to a title search.
/// </summary>
/// <param name="show">The show to search for</param>
/// <returns>A show containing metadata from TheMovieDb</returns>
private async Task<Show> _GetShow(Show show)
{
if (!show.TryGetID(Provider.Slug, out int id))
{
Show found = (await _SearchShows(show.Title ?? show.Slug, show.StartAir?.Year))
.FirstOrDefault(x => x.IsMovie == show.IsMovie);
if (found?.TryGetID(Provider.Slug, out id) != true)
return found;
}
TMDbClient client = new(_apiKey.Value.ApiKey);
if (show.IsMovie)
{
return (await client
.GetMovieAsync(id, MovieMethods.AlternativeTitles | MovieMethods.Videos | MovieMethods.Credits))
?.ToShow(Provider);
}
return (await client
.GetTvShowAsync(id, TvShowMethods.AlternativeTitles | TvShowMethods.Videos | TvShowMethods.Credits))
?.ToShow(Provider);
}
/// <summary>
/// Get a season using it's show and it's season number.
/// </summary>
/// <param name="season">The season to retrieve metadata for.</param>
/// <returns>A season containing metadata from TheMovieDb</returns>
private async Task<Season> _GetSeason(Season season)
{
if (season.Show == null)
{
_logger.LogWarning("Metadata for a season was requested but it's show is not loaded. " +
"This is unsupported");
return null;
}
if (!season.Show.TryGetID(Provider.Slug, out int id))
return null;
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.GetTvSeasonAsync(id, season.SeasonNumber))
.ToSeason(id, Provider);
}
/// <summary>
/// Get an episode using it's show, it's season number and it's episode number.
/// Absolute numbering is not supported.
/// </summary>
/// <param name="episode">The episode to retrieve metadata for.</param>
/// <returns>An episode containing metadata from TheMovieDb</returns>
private async Task<Episode> _GetEpisode(Episode episode)
{
if (episode.Show == null)
{
_logger.LogWarning("Metadata for an episode was requested but it's show is not loaded. " +
"This is unsupported");
return null;
}
if (!episode.Show.TryGetID(Provider.Slug, out int id)
|| episode.SeasonNumber == null || episode.EpisodeNumber == null)
return null;
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.GetTvEpisodeAsync(id, episode.SeasonNumber.Value, episode.EpisodeNumber.Value))
.ToEpisode(id, Provider);
}
/// <summary>
/// Get a person using it's id, if the id is not present in the person, fallback to a name search.
/// </summary>
/// <param name="person">The person to search for</param>
/// <returns>A person containing metadata from TheMovieDb</returns>
private async Task<People> _GetPerson(People person)
{
if (!person.TryGetID(Provider.Slug, out int id))
{
People found = (await _SearchPeople(person.Name ?? person.Slug)).FirstOrDefault();
if (found?.TryGetID(Provider.Slug, out id) != true)
return found;
}
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.GetPersonAsync(id)).ToPeople(Provider);
}
/// <summary>
/// Get a studio using it's id, if the id is not present in the studio, fallback to a name search.
/// </summary>
/// <param name="studio">The studio to search for</param>
/// <returns>A studio containing metadata from TheMovieDb</returns>
private async Task<Studio> _GetStudio(Studio studio)
{
if (!studio.TryGetID(Provider.Slug, out int id))
{
Studio found = (await _SearchStudios(studio.Name ?? studio.Slug)).FirstOrDefault();
if (found?.TryGetID(Provider.Slug, out id) != true)
return found;
}
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.GetCompanyAsync(id)).ToStudio(Provider);
}
/// <inheritdoc />
public async Task<ICollection<T>> Search<T>(string query)
where T : class, IResource
{
if (typeof(T) == typeof(Collection))
return (await _SearchCollections(query) as ICollection<T>)!;
if (typeof(T) == typeof(Show))
return (await _SearchShows(query) as ICollection<T>)!;
if (typeof(T) == typeof(People))
return (await _SearchPeople(query) as ICollection<T>)!;
if (typeof(T) == typeof(Studio))
return (await _SearchStudios(query) as ICollection<T>)!;
return ArraySegment<T>.Empty;
}
/// <summary>
/// Search for a collection using it's name as a query.
/// </summary>
/// <param name="query">The query to search for</param>
/// <returns>A list of collections containing metadata from TheMovieDb</returns>
private async Task<ICollection<Collection>> _SearchCollections(string query)
{
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.SearchCollectionAsync(query))
.Results
.Select(x => x.ToCollection(Provider))
.ToArray();
}
/// <summary>
/// Search for a show using it's name as a query.
/// </summary>
/// <param name="query">The query to search for</param>
/// <param name="year">The year in witch the show has aired.</param>
/// <returns>A list of shows containing metadata from TheMovieDb</returns>
private async Task<ICollection<Show>> _SearchShows(string query, int? year = null)
{
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.SearchMultiAsync(query, year: year ?? 0))
.Results
.Select(x =>
{
return x switch
{
SearchTv tv => tv.ToShow(Provider),
SearchMovie movie => movie.ToShow(Provider),
_ => null
};
})
.Where(x => x != null)
.ToArray();
}
/// <summary>
/// Search for people using there name as a query.
/// </summary>
/// <param name="query">The query to search for</param>
/// <returns>A list of people containing metadata from TheMovieDb</returns>
private async Task<ICollection<People>> _SearchPeople(string query)
{
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.SearchPersonAsync(query))
.Results
.Select(x => x.ToPeople(Provider))
.ToArray();
}
/// <summary>
/// Search for studios using there name as a query.
/// </summary>
/// <param name="query">The query to search for</param>
/// <returns>A list of studios containing metadata from TheMovieDb</returns>
private async Task<ICollection<Studio>> _SearchStudios(string query)
{
TMDbClient client = new(_apiKey.Value.ApiKey);
return (await client.SearchCompanyAsync(query))
.Results
.Select(x => x.ToStudio(Provider))
.ToArray();
}
}
}

View File

@ -0,0 +1,18 @@
namespace Kyoo.TheMovieDb.Models
{
/// <summary>
/// The option containing the api key for TheMovieDb.
/// </summary>
public class TheMovieDbOptions
{
/// <summary>
/// The path to get this option from the root configuration.
/// </summary>
public const string Path = "the-moviedb";
/// <summary>
/// The api key of TheMovieDb.
/// </summary>
public string ApiKey { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Kyoo.Models; using Kyoo.Models;
@ -47,7 +48,7 @@ namespace Kyoo.TheTvdb
/// <returns>A show representing the given search result.</returns> /// <returns>A show representing the given search result.</returns>
public static Show ToShow(this SeriesSearchResult result, Provider provider) public static Show ToShow(this SeriesSearchResult result, Provider provider)
{ {
return new() return new Show
{ {
Slug = result.Slug, Slug = result.Slug,
Title = result.SeriesName, Title = result.SeriesName,
@ -55,14 +56,19 @@ namespace Kyoo.TheTvdb
Overview = result.Overview, Overview = result.Overview,
Status = _GetStatus(result.Status), Status = _GetStatus(result.Status),
StartAir = _ParseDate(result.FirstAired), StartAir = _ParseDate(result.FirstAired),
Poster = result.Poster != null ? $"https://www.thetvdb.com{result.Poster}" : null, Images = new Dictionary<int, string>
{
[Images.Poster] = !string.IsNullOrEmpty(result.Poster)
? $"https://www.thetvdb.com{result.Poster}"
: null,
},
ExternalIDs = new[] ExternalIDs = new[]
{ {
new MetadataID<Show> new MetadataID
{ {
DataID = result.Id.ToString(), DataID = result.Id.ToString(),
Link = $"https://www.thetvdb.com/series/{result.Slug}", Link = $"https://www.thetvdb.com/series/{result.Slug}",
Second = provider Provider = provider
} }
} }
}; };
@ -76,7 +82,7 @@ namespace Kyoo.TheTvdb
/// <returns>A show representing the given series.</returns> /// <returns>A show representing the given series.</returns>
public static Show ToShow(this Series series, Provider provider) public static Show ToShow(this Series series, Provider provider)
{ {
return new() return new Show
{ {
Slug = series.Slug, Slug = series.Slug,
Title = series.SeriesName, Title = series.SeriesName,
@ -84,16 +90,23 @@ namespace Kyoo.TheTvdb
Overview = series.Overview, Overview = series.Overview,
Status = _GetStatus(series.Status), Status = _GetStatus(series.Status),
StartAir = _ParseDate(series.FirstAired), StartAir = _ParseDate(series.FirstAired),
Poster = series.Poster != null ? $"https://www.thetvdb.com/banners/{series.Poster}" : null, Images = new Dictionary<int, string>
Backdrop = series.FanArt != null ? $"https://www.thetvdb.com/banners/{series.FanArt}" : null, {
[Images.Poster] = !string.IsNullOrEmpty(series.Poster)
? $"https://www.thetvdb.com/banners/{series.Poster}"
: null,
[Images.Thumbnail] = !string.IsNullOrEmpty(series.FanArt)
? $"https://www.thetvdb.com/banners/{series.FanArt}"
: null
},
Genres = series.Genre.Select(y => new Genre(y)).ToList(), Genres = series.Genre.Select(y => new Genre(y)).ToList(),
ExternalIDs = new[] ExternalIDs = new[]
{ {
new MetadataID<Show> new MetadataID
{ {
DataID = series.Id.ToString(), DataID = series.Id.ToString(),
Link = $"https://www.thetvdb.com/series/{series.Slug}", Link = $"https://www.thetvdb.com/series/{series.Slug}",
Second = provider Provider = provider
} }
} }
}; };
@ -103,25 +116,20 @@ namespace Kyoo.TheTvdb
/// Convert a tvdb actor to a kyoo <see cref="PeopleRole"/>. /// Convert a tvdb actor to a kyoo <see cref="PeopleRole"/>.
/// </summary> /// </summary>
/// <param name="actor">The actor to convert</param> /// <param name="actor">The actor to convert</param>
/// <param name="provider">The provider representing the tvdb inside kyoo</param>
/// <returns>A people role representing the given actor in the role they played.</returns> /// <returns>A people role representing the given actor in the role they played.</returns>
public static PeopleRole ToPeopleRole(this Actor actor, Provider provider) public static PeopleRole ToPeopleRole(this Actor actor)
{ {
return new() return new PeopleRole
{ {
People = new People People = new People
{ {
Slug = Utility.ToSlug(actor.Name), Slug = Utility.ToSlug(actor.Name),
Name = actor.Name, Name = actor.Name,
Poster = actor.Image != null ? $"https://www.thetvdb.com/banners/{actor.Image}" : null, Images = new Dictionary<int, string>
ExternalIDs = new []
{ {
new MetadataID<People>() [Images.Poster] = !string.IsNullOrEmpty(actor.Image)
{ ? $"https://www.thetvdb.com/banners/{actor.Image}"
DataID = actor.Id.ToString(), : null
Link = $"https://www.thetvdb.com/people/{actor.Id}",
Second = provider
}
} }
}, },
Role = actor.Role, Role = actor.Role,
@ -137,21 +145,26 @@ namespace Kyoo.TheTvdb
/// <returns>A episode representing the given tvdb episode.</returns> /// <returns>A episode representing the given tvdb episode.</returns>
public static Episode ToEpisode(this EpisodeRecord episode, Provider provider) public static Episode ToEpisode(this EpisodeRecord episode, Provider provider)
{ {
return new() return new Episode
{ {
SeasonNumber = episode.AiredSeason, SeasonNumber = episode.AiredSeason,
EpisodeNumber = episode.AiredEpisodeNumber, EpisodeNumber = episode.AiredEpisodeNumber,
AbsoluteNumber = episode.AbsoluteNumber, AbsoluteNumber = episode.AbsoluteNumber,
Title = episode.EpisodeName, Title = episode.EpisodeName,
Overview = episode.Overview, Overview = episode.Overview,
Thumb = episode.Filename != null ? $"https://www.thetvdb.com/banners/{episode.Filename}" : null, Images = new Dictionary<int, string>
{
[Images.Thumbnail] = !string.IsNullOrEmpty(episode.Filename)
? $"https://www.thetvdb.com/banners/{episode.Filename}"
: null
},
ExternalIDs = new[] ExternalIDs = new[]
{ {
new MetadataID<Episode> new MetadataID
{ {
DataID = episode.Id.ToString(), DataID = episode.Id.ToString(),
Link = $"https://www.thetvdb.com/series/{episode.SeriesId}/episodes/{episode.Id}", Link = $"https://www.thetvdb.com/series/{episode.SeriesId}/episodes/{episode.Id}",
Second = provider Provider = provider
} }
} }
}; };

View File

@ -32,8 +32,10 @@ namespace Kyoo.TheTvdb
{ {
Slug = "the-tvdb", Slug = "the-tvdb",
Name = "TheTVDB", Name = "TheTVDB",
LogoExtension = "png", Images = new Dictionary<int, string>
Logo = "https://www.thetvdb.com/images/logo.png" {
[Images.Logo] = "https://www.thetvdb.com/images/logo.png"
}
}; };
@ -93,7 +95,7 @@ namespace Kyoo.TheTvdb
Show ret = series.Data.ToShow(Provider); Show ret = series.Data.ToShow(Provider);
TvDbResponse<Actor[]> people = await _client.Series.GetActorsAsync(id); TvDbResponse<Actor[]> people = await _client.Series.GetActorsAsync(id);
ret.People = people.Data.Select(x => x.ToPeopleRole(Provider)).ToArray(); ret.People = people.Data.Select(x => x.ToPeopleRole()).ToArray();
return ret; return ret;
} }

@ -1 +1 @@
Subproject commit c037270d3339fcf0075984a089f353c5c332a751 Subproject commit dca10903ff54a8999732695b5c2a0a5c94f85200

View File

@ -5,8 +5,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Common", "Kyoo.Common\
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.CommonAPI", "Kyoo.CommonAPI\Kyoo.CommonAPI.csproj", "{6F91B645-F785-46BB-9C4F-1EFC83E489B6}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.CommonAPI", "Kyoo.CommonAPI\Kyoo.CommonAPI.csproj", "{6F91B645-F785-46BB-9C4F-1EFC83E489B6}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "Kyoo.Tests\Kyoo.Tests.csproj", "{D179D5FF-9F75-4B27-8E27-0DBDF1806611}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}"
@ -15,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheTvdb", "Kyoo.TheTvdb\Kyoo.TheTvdb.csproj", "{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheTvdb", "Kyoo.TheTvdb\Kyoo.TheTvdb.csproj", "{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheMovieDb", "Kyoo.TheMovieDb\Kyoo.TheMovieDb.csproj", "{BAB270D4-E0EA-4329-BA65-512FDAB01001}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "tests\Kyoo.Tests\Kyoo.Tests.csproj", "{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -33,10 +35,6 @@ Global
{6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Release|Any CPU.Build.0 = Release|Any CPU {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Release|Any CPU.Build.0 = Release|Any CPU
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Release|Any CPU.Build.0 = Release|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -53,5 +51,13 @@ Global
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.Build.0 = Release|Any CPU {D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.Build.0 = Release|Any CPU
{BAB270D4-E0EA-4329-BA65-512FDAB01001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BAB270D4-E0EA-4329-BA65-512FDAB01001}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BAB270D4-E0EA-4329-BA65-512FDAB01001}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BAB270D4-E0EA-4329-BA65-512FDAB01001}.Release|Any CPU.Build.0 = Release|Any CPU
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,11 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_STYLE/@EntryValue">Tab</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue">API</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -8,7 +8,9 @@ using Autofac.Features.Metadata;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Common.Models.Attributes; using Kyoo.Common.Models.Attributes;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Options;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
@ -23,14 +25,31 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private readonly ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> _fileSystems; private readonly ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> _fileSystems;
/// <summary>
/// The library manager used to load shows to retrieve their path
/// (only if the option is set to metadata in show)
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
/// </summary>
private readonly IOptionsMonitor<BasicOptions> _options;
/// <summary> /// <summary>
/// Create a new <see cref="FileSystemComposite"/> from a list of <see cref="IFileSystem"/> mapped to their /// Create a new <see cref="FileSystemComposite"/> from a list of <see cref="IFileSystem"/> mapped to their
/// metadata. /// metadata.
/// </summary> /// </summary>
/// <param name="fileSystems">The list of filesystem mapped to their metadata.</param> /// <param name="fileSystems">The list of filesystem mapped to their metadata.</param>
public FileSystemComposite(ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> fileSystems) /// <param name="libraryManager">The library manager used to load shows to retrieve their path.</param>
/// <param name="options">The options to use.</param>
public FileSystemComposite(ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> fileSystems,
ILibraryManager libraryManager,
IOptionsMonitor<BasicOptions> options)
{ {
_fileSystems = fileSystems; _fileSystems = fileSystems;
_libraryManager = libraryManager;
_options = options;
} }
@ -88,6 +107,15 @@ namespace Kyoo.Controllers
.GetReader(relativePath); .GetReader(relativePath);
} }
/// <inheritdoc />
public Task<Stream> GetReader(string path, AsyncRef<string> mime)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.GetReader(relativePath, mime);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Stream> NewFile(string path) public Task<Stream> NewFile(string path)
{ {
@ -132,12 +160,41 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public string GetExtraDirectory(Show show) public async Task<string> GetExtraDirectory<T>(T resource)
{ {
if (show == null) switch (resource)
throw new ArgumentNullException(nameof(show)); {
return _GetFileSystemForPath(show.Path, out string _) case Season season:
.GetExtraDirectory(show); await _libraryManager.Load(season, x => x.Show);
break;
case Episode episode:
await _libraryManager.Load(episode, x => x.Show);
break;
case Track track:
await _libraryManager.Load(track, x => x.Episode);
await _libraryManager.Load(track.Episode, x => x.Show);
break;
}
IFileSystem fs = resource switch
{
Show show => _GetFileSystemForPath(show.Path, out string _),
Season season => _GetFileSystemForPath(season.Show.Path, out string _),
Episode episode => _GetFileSystemForPath(episode.Show.Path, out string _),
Track track => _GetFileSystemForPath(track.Episode.Show.Path, out string _),
_ => _GetFileSystemForPath(_options.CurrentValue.MetadataPath, out string _)
};
string path = await fs.GetExtraDirectory(resource)
?? resource switch
{
Season season => await GetExtraDirectory(season.Show),
Episode episode => await GetExtraDirectory(episode.Show),
Track track => await GetExtraDirectory(track.Episode),
IResource res => Combine(_options.CurrentValue.MetadataPath,
typeof(T).Name.ToLowerInvariant(), res.Slug),
_ => Combine(_options.CurrentValue.MetadataPath, typeof(T).Name.ToLowerInvariant())
};
return await CreateDirectory(path);
} }
} }
} }

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Common.Models.Attributes; using Kyoo.Common.Models.Attributes;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Controllers namespace Kyoo.Controllers
@ -45,6 +45,16 @@ namespace Kyoo.Controllers
return client.GetStreamAsync(path); return client.GetStreamAsync(path);
} }
/// <inheritdoc />
public async Task<Stream> GetReader(string path, AsyncRef<string> mime)
{
HttpClient client = _clientFactory.CreateClient();
HttpResponseMessage response = await client.GetAsync(path);
response.EnsureSuccessStatusCode();
mime.Value = response.Content.Headers.ContentType?.MediaType;
return await response.Content.ReadAsStreamAsync();
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Stream> NewFile(string path) public Task<Stream> NewFile(string path)
{ {
@ -76,7 +86,7 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public string GetExtraDirectory(Show show) public Task<string> GetExtraDirectory<T>(T resource)
{ {
throw new NotSupportedException("Extras can not be stored inside an http filesystem."); throw new NotSupportedException("Extras can not be stored inside an http filesystem.");
} }
@ -85,6 +95,8 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// An <see cref="IActionResult"/> to proxy an http request. /// An <see cref="IActionResult"/> to proxy an http request.
/// </summary> /// </summary>
// TODO remove this suppress message once the class has been implemented.
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
public class HttpForwardResult : IActionResult public class HttpForwardResult : IActionResult
{ {
/// <summary> /// <summary>

View File

@ -4,8 +4,10 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Common.Models.Attributes; using Kyoo.Common.Models.Attributes;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Options;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
@ -20,6 +22,20 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private FileExtensionContentTypeProvider _provider; private FileExtensionContentTypeProvider _provider;
/// <summary>
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
/// </summary>
private readonly IOptionsMonitor<BasicOptions> _options;
/// <summary>
/// Create a new <see cref="LocalFileSystem"/> with the specified options.
/// </summary>
/// <param name="options">The options to use.</param>
public LocalFileSystem(IOptionsMonitor<BasicOptions> options)
{
_options = options;
}
/// <summary> /// <summary>
/// Get the content type of a file using it's extension. /// Get the content type of a file using it's extension.
/// </summary> /// </summary>
@ -63,6 +79,16 @@ namespace Kyoo.Controllers
return Task.FromResult<Stream>(File.OpenRead(path)); return Task.FromResult<Stream>(File.OpenRead(path));
} }
/// <inheritdoc />
public Task<Stream> GetReader(string path, AsyncRef<string> mime)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
_provider.TryGetContentType(path, out string mimeValue);
mime.Value = mimeValue;
return Task.FromResult<Stream>(File.OpenRead(path));
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Stream> NewFile(string path) public Task<Stream> NewFile(string path)
{ {
@ -104,11 +130,18 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public string GetExtraDirectory(Show show) public Task<string> GetExtraDirectory<T>(T resource)
{ {
string path = Path.Combine(show.Path, "Extra"); if (!_options.CurrentValue.MetadataInShow)
Directory.CreateDirectory(path); return Task.FromResult<string>(null);
return path; return Task.FromResult(resource switch
{
Show show => Combine(show.Path, "Extra"),
Season season => Combine(season.Show.Path, "Extra"),
Episode episode => Combine(episode.Show.Path, "Extra"),
Track track => Combine(track.Episode.Show.Path, "Extra"),
_ => null
});
} }
} }
} }

View File

@ -18,6 +18,11 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc /> /// <inheritdoc />
protected override Expression<Func<Collection, object>> DefaultSort => x => x.Name; protected override Expression<Func<Collection, object>> DefaultSort => x => x.Name;
@ -25,17 +30,19 @@ namespace Kyoo.Controllers
/// Create a new <see cref="CollectionRepository"/>. /// Create a new <see cref="CollectionRepository"/>.
/// </summary> /// </summary>
/// <param name="database">The database handle to use</param> /// <param name="database">The database handle to use</param>
public CollectionRepository(DatabaseContext database) /// /// <param name="providers">A provider repository</param>
public CollectionRepository(DatabaseContext database, IProviderRepository providers)
: base(database) : base(database)
{ {
_database = database; _database = database;
_providers = providers;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Collection>> Search(string query) public override async Task<ICollection<Collection>> Search(string query)
{ {
return await _database.Collections return await _database.Collections
.Where(_database.Like<Collection>(x => x.Name, $"%{query}%")) .Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
.OrderBy(DefaultSort) .OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
@ -50,6 +57,40 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task Validate(Collection resource)
{
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 /> /// <inheritdoc />
public override async Task Delete(Collection obj) public override async Task Delete(Collection obj)
{ {

View File

@ -99,7 +99,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Episode>> Search(string query) public override async Task<ICollection<Episode>> Search(string query)
{ {
return await _database.Episodes return await _database.Episodes
.Where(x => x.EpisodeNumber != null) .Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null)
.Where(_database.Like<Episode>(x => x.Title, $"%{query}%")) .Where(_database.Like<Episode>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort) .OrderBy(DefaultSort)
.Take(20) .Take(20)
@ -111,7 +111,6 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
return await ValidateTracks(obj); return await ValidateTracks(obj);
} }
@ -119,8 +118,7 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld) protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
{ {
if (resource.ShowID <= 0) await Validate(changed);
throw new InvalidOperationException($"Can't store an episode not related to any show (showID: {resource.ShowID}).");
if (changed.Tracks != null || resetOld) if (changed.Tracks != null || resetOld)
{ {
@ -134,8 +132,6 @@ namespace Kyoo.Controllers
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs; resource.ExternalIDs = changed.ExternalIDs;
} }
await Validate(resource);
} }
/// <summary> /// <summary>
@ -145,12 +141,16 @@ namespace Kyoo.Controllers
/// <returns>The <see cref="resource"/> parameter is returned.</returns> /// <returns>The <see cref="resource"/> parameter is returned.</returns>
private async Task<Episode> ValidateTracks(Episode resource) private async Task<Episode> ValidateTracks(Episode resource)
{ {
resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.SelectAsync(x => if (resource.Tracks == null)
return resource;
resource.Tracks = await resource.Tracks.SelectAsync(x =>
{ {
x.Episode = resource; x.Episode = resource;
x.EpisodeSlug = resource.Slug; x.EpisodeSlug = resource.Slug;
return _tracks.Create(x); return _tracks.Create(x);
}).ToListAsync()); }).ToListAsync();
_database.Tracks.AttachRange(resource.Tracks);
return resource; return resource;
} }
@ -158,12 +158,24 @@ namespace Kyoo.Controllers
protected override async Task Validate(Episode resource) protected override async Task Validate(Episode resource)
{ {
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async x => if (resource.ShowID <= 0)
{ {
x.Second = await _providers.CreateIfNotExists(x.Second); if (resource.Show == null)
x.SecondID = x.Second.ID; throw new ArgumentException($"Can't store an episode not related " +
_database.Entry(x.Second).State = EntityState.Detached; $"to any show (showID: {resource.ShowID}).");
}); 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 /> /// <inheritdoc />

View File

@ -43,7 +43,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Library>> Search(string query) public override async Task<ICollection<Library>> Search(string query)
{ {
return await _database.Libraries return await _database.Libraries
.Where(_database.Like<Library>(x => x.Name, $"%{query}%")) .Where(_database.Like<Library>(x => x.Name + " " + x.Slug, $"%{query}%"))
.OrderBy(DefaultSort) .OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
@ -54,7 +54,6 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
obj.ProviderLinks.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -63,20 +62,7 @@ namespace Kyoo.Controllers
protected override async Task Validate(Library resource) protected override async Task Validate(Library resource)
{ {
await base.Validate(resource); await base.Validate(resource);
resource.ProviderLinks = resource.Providers?
.Select(x => Link.Create(resource, x))
.ToList();
await resource.ProviderLinks.ForEachAsync(async id =>
{
id.Second = await _providers.CreateIfNotExists(id.Second);
id.SecondID = id.Second.ID;
_database.Entry(id.Second).State = EntityState.Detached;
});
}
/// <inheritdoc />
protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{
if (string.IsNullOrEmpty(resource.Slug)) if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("The library's slug must be set and not empty"); throw new ArgumentException("The library's slug must be set and not empty");
if (string.IsNullOrEmpty(resource.Name)) if (string.IsNullOrEmpty(resource.Name))
@ -84,9 +70,22 @@ namespace Kyoo.Controllers
if (resource.Paths == null || !resource.Paths.Any()) if (resource.Paths == null || !resource.Paths.Any())
throw new ArgumentException("The library should have a least one path."); throw new ArgumentException("The library should have a least one path.");
if (changed.Providers != null || resetOld) if (resource.Providers != null)
{
resource.Providers = await resource.Providers
.SelectAsync(x => _providers.CreateIfNotExists(x))
.ToListAsync();
_database.AttachRange(resource.Providers);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{ {
await Validate(changed); await Validate(changed);
if (changed.Providers != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.Providers).LoadAsync(); await Database.Entry(resource).Collection(x => x.Providers).LoadAsync();
resource.Providers = changed.Providers; resource.Providers = changed.Providers;
} }

View File

@ -62,7 +62,6 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -71,23 +70,35 @@ namespace Kyoo.Controllers
protected override async Task Validate(People resource) protected override async Task Validate(People resource)
{ {
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id =>
if (resource.ExternalIDs != null)
{ {
id.Second = await _providers.CreateIfNotExists(id.Second); foreach (MetadataID id in resource.ExternalIDs)
id.SecondID = id.Second.ID;
_database.Entry(id.Second).State = EntityState.Detached;
});
await resource.Roles.ForEachAsync(async role =>
{ {
role.Show = await _shows.Value.CreateIfNotExists(role.Show); 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)
{
role.Show = _database.LocalEntity<Show>(role.Show.Slug)
?? await _shows.Value.CreateIfNotExists(role.Show);
role.ShowID = role.Show.ID; role.ShowID = role.Show.ID;
_database.Entry(role.Show).State = EntityState.Detached; _database.Entry(role).State = EntityState.Added;
}); }
}
} }
/// <inheritdoc /> /// <inheritdoc />
protected override async Task EditRelations(People resource, People changed, bool resetOld) protected override async Task EditRelations(People resource, People changed, bool resetOld)
{ {
await Validate(changed);
if (changed.Roles != null || resetOld) if (changed.Roles != null || resetOld)
{ {
await Database.Entry(resource).Collection(x => x.Roles).LoadAsync(); await Database.Entry(resource).Collection(x => x.Roles).LoadAsync();
@ -98,9 +109,7 @@ namespace Kyoo.Controllers
{ {
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs; resource.ExternalIDs = changed.ExternalIDs;
} }
await base.EditRelations(resource, changed, resetOld);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -18,9 +18,6 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<Provider, object>> DefaultSort => x => x.Slug;
/// <summary> /// <summary>
/// Create a new <see cref="ProviderRepository" />. /// Create a new <see cref="ProviderRepository" />.
@ -32,6 +29,9 @@ namespace Kyoo.Controllers
_database = database; _database = database;
} }
/// <inheritdoc />
protected override Expression<Func<Provider, object>> DefaultSort => x => x.Slug;
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Provider>> Search(string query) public override async Task<ICollection<Provider>> Search(string query)
{ {
@ -47,7 +47,8 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated provider (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync("Trying to insert a duplicated provider " +
$"(slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -62,14 +63,15 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null, public Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID<T>> sort = default, Sort<MetadataID> sort = default,
Pagination limit = default) Pagination limit = default)
where T : class, IResource where T : class, IMetadata
{ {
return ApplyFilters(_database.MetadataIds<T>().Include(y => y.Second), return ApplyFilters(_database.MetadataIds<T>()
x => _database.MetadataIds<T>().FirstOrDefaultAsync(y => y.FirstID == x), .Include(y => y.Provider),
x => x.FirstID, x => _database.MetadataIds<T>().FirstOrDefaultAsync(y => y.ResourceID == x),
x => x.ResourceID,
where, where,
sort, sort,
limit); limit);

View File

@ -87,7 +87,6 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated season (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated season (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -95,32 +94,37 @@ namespace Kyoo.Controllers
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task Validate(Season resource) protected override async Task Validate(Season resource)
{ {
await base.Validate(resource);
if (resource.ShowID <= 0) if (resource.ShowID <= 0)
{ {
if (resource.Show == null) if (resource.Show == null)
throw new InvalidOperationException( throw new ArgumentException(
$"Can't store a season not related to any show (showID: {resource.ShowID})."); $"Can't store a season not related to any show (showID: {resource.ShowID}).");
resource.ShowID = resource.Show.ID; resource.ShowID = resource.Show.ID;
} }
await base.Validate(resource); if (resource.ExternalIDs != null)
await resource.ExternalIDs.ForEachAsync(async id =>
{ {
id.Second = await _providers.CreateIfNotExists(id.Second); foreach (MetadataID id in resource.ExternalIDs)
id.SecondID = id.Second.ID; {
_database.Entry(id.Second).State = EntityState.Detached; 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/> /// <inheritdoc/>
protected override async Task EditRelations(Season resource, Season changed, bool resetOld) protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
{ {
await Validate(changed);
if (changed.ExternalIDs != null || resetOld) if (changed.ExternalIDs != null || resetOld)
{ {
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs; resource.ExternalIDs = changed.ExternalIDs;
} }
await base.EditRelations(resource, changed, resetOld);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -76,9 +76,6 @@ namespace Kyoo.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
obj.GenreLinks.ForEach(x => _database.Entry(x).State = EntityState.Added);
obj.People.ForEach(x => _database.Entry(x).State = EntityState.Added);
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated show (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated show (slug {obj.Slug} already exists).");
return obj; return obj;
} }
@ -88,29 +85,40 @@ namespace Kyoo.Controllers
{ {
await base.Validate(resource); await base.Validate(resource);
if (resource.Studio != null) if (resource.Studio != null)
{
resource.Studio = await _studios.CreateIfNotExists(resource.Studio); resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.StudioID = resource.Studio.ID;
}
resource.GenreLinks = resource.Genres? if (resource.Genres != null)
.Select(x => Link.Create(resource, x))
.ToList();
await resource.GenreLinks.ForEachAsync(async id =>
{ {
id.Second = await _genres.CreateIfNotExists(id.Second); resource.Genres = await resource.Genres
id.SecondID = id.Second.ID; .SelectAsync(x => _genres.CreateIfNotExists(x))
_database.Entry(id.Second).State = EntityState.Detached; .ToListAsync();
}); _database.AttachRange(resource.Genres);
await resource.ExternalIDs.ForEachAsync(async id => }
if (resource.ExternalIDs != null)
{ {
id.Second = await _providers.CreateIfNotExists(id.Second); foreach (MetadataID id in resource.ExternalIDs)
id.SecondID = id.Second.ID;
_database.Entry(id.Second).State = EntityState.Detached;
});
await resource.People.ForEachAsync(async role =>
{ {
role.People = await _people.CreateIfNotExists(role.People); 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)
{
role.People = _database.LocalEntity<People>(role.People.Slug)
?? await _people.CreateIfNotExists(role.People);
role.PeopleID = role.People.ID; role.PeopleID = role.People.ID;
_database.Entry(role.People).State = EntityState.Detached; _database.Entry(role).State = EntityState.Added;
}); }
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -151,21 +159,18 @@ namespace Kyoo.Controllers
{ {
if (collectionID != null) if (collectionID != null)
{ {
await _database.Links<Collection, Show>() await _database.AddLinks<Collection, Show>(collectionID.Value, showID);
.AddAsync(new Link<Collection, Show>(collectionID.Value, showID));
await _database.SaveIfNoDuplicates(); await _database.SaveIfNoDuplicates();
if (libraryID != null) if (libraryID != null)
{ {
await _database.Links<Library, Collection>() await _database.AddLinks<Library, Collection>(libraryID.Value, collectionID.Value);
.AddAsync(new Link<Library, Collection>(libraryID.Value, collectionID.Value));
await _database.SaveIfNoDuplicates(); await _database.SaveIfNoDuplicates();
} }
} }
if (libraryID != null) if (libraryID != null)
{ {
await _database.Links<Library, Show>() await _database.AddLinks<Library, Show>(libraryID.Value, showID);
.AddAsync(new Link<Library, Show>(libraryID.Value, showID));
await _database.SaveIfNoDuplicates(); await _database.SaveIfNoDuplicates();
} }
} }

View File

@ -18,6 +18,11 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc /> /// <inheritdoc />
protected override Expression<Func<Studio, object>> DefaultSort => x => x.Name; protected override Expression<Func<Studio, object>> DefaultSort => x => x.Name;
@ -26,10 +31,12 @@ namespace Kyoo.Controllers
/// Create a new <see cref="StudioRepository"/>. /// Create a new <see cref="StudioRepository"/>.
/// </summary> /// </summary>
/// <param name="database">The database handle</param> /// <param name="database">The database handle</param>
public StudioRepository(DatabaseContext database) /// <param name="providers">A provider repository</param>
public StudioRepository(DatabaseContext database, IProviderRepository providers)
: base(database) : base(database)
{ {
_database = database; _database = database;
_providers = providers;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -51,6 +58,34 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task Validate(Studio resource)
{
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 /> /// <inheritdoc />
public override async Task Delete(Studio obj) public override async Task Delete(Studio obj)
{ {

View File

@ -37,19 +37,25 @@ namespace Kyoo.Controllers
throw new InvalidOperationException("Tracks do not support the search method."); throw new InvalidOperationException("Tracks do not support the search method.");
} }
/// <inheritdoc />
protected override async Task Validate(Track resource)
{
await base.Validate(resource);
if (resource.EpisodeID <= 0)
{
resource.EpisodeID = resource.Episode?.ID ?? 0;
if (resource.EpisodeID <= 0)
throw new ArgumentException("Can't store a track not related to any episode " +
$"(episodeID: {resource.EpisodeID}).");
}
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<Track> Create(Track obj) public override async Task<Track> Create(Track obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
if (obj.EpisodeID <= 0)
{
obj.EpisodeID = obj.Episode?.ID ?? 0;
if (obj.EpisodeID <= 0)
throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID}).");
}
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();

View File

@ -1,11 +1,10 @@
using Kyoo.Models; using Kyoo.Models;
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using Microsoft.AspNetCore.StaticFiles;
using Kyoo.Models.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
@ -22,54 +21,17 @@ namespace Kyoo.Controllers
/// A logger to report errors. /// A logger to report errors.
/// </summary> /// </summary>
private readonly ILogger<ThumbnailsManager> _logger; private readonly ILogger<ThumbnailsManager> _logger;
/// <summary>
/// The options containing the base path of people images and provider logos.
/// </summary>
private readonly IOptionsMonitor<BasicOptions> _options;
/// <summary>
/// A library manager used to load episode and seasons shows if they are not loaded.
/// </summary>
private readonly Lazy<ILibraryManager> _library;
/// <summary> /// <summary>
/// Create a new <see cref="ThumbnailsManager"/>. /// Create a new <see cref="ThumbnailsManager"/>.
/// </summary> /// </summary>
/// <param name="files">The file manager to use.</param> /// <param name="files">The file manager to use.</param>
/// <param name="logger">A logger to report errors</param> /// <param name="logger">A logger to report errors</param>
/// <param name="options">The options to use.</param>
/// <param name="library">A library manager used to load shows if they are not loaded.</param>
public ThumbnailsManager(IFileSystem files, public ThumbnailsManager(IFileSystem files,
ILogger<ThumbnailsManager> logger, ILogger<ThumbnailsManager> logger)
IOptionsMonitor<BasicOptions> options,
Lazy<ILibraryManager> library)
{ {
_files = files; _files = files;
_logger = logger; _logger = logger;
_options = options;
_library = library;
options.OnChange(x =>
{
_files.CreateDirectory(x.PeoplePath);
_files.CreateDirectory(x.ProviderPath);
});
}
/// <inheritdoc />
public Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
where T : IResource
{
if (item == null)
throw new ArgumentNullException(nameof(item));
return item switch
{
Show show => _Validate(show, alwaysDownload),
Season season => _Validate(season, alwaysDownload),
Episode episode => _Validate(episode, alwaysDownload),
People people => _Validate(people, alwaysDownload),
Provider provider => _Validate(provider, alwaysDownload),
_ => Task.FromResult(false)
};
} }
/// <summary> /// <summary>
@ -86,8 +48,12 @@ namespace Kyoo.Controllers
try try
{ {
await using Stream reader = await _files.GetReader(url); AsyncRef<string> mime = new();
await using Stream local = await _files.NewFile(localPath); await using Stream reader = await _files.GetReader(url, mime);
string extension = new FileExtensionContentTypeProvider()
.Mappings.FirstOrDefault(x => x.Value == mime.Value)
.Key;
await using Stream local = await _files.NewFile(localPath + extension);
await reader.CopyToAsync(local); await reader.CopyToAsync(local);
return true; return true;
} }
@ -98,195 +64,74 @@ namespace Kyoo.Controllers
} }
} }
/// <summary> /// <inheritdoc />
/// Download images of a specified show. public async Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
/// </summary> where T : IThumbnails
/// <param name="show">
/// 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>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _Validate([NotNull] Show show, bool alwaysDownload)
{ {
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; bool ret = false;
if (show.Poster != null) foreach ((int id, string image) in item.Images.Where(x => x.Value != null))
{ {
string posterPath = await GetPoster(show); string localPath = await _GetPrivateImagePath(item, id);
if (alwaysDownload || !await _files.Exists(posterPath)) if (alwaysDownload || !await _files.Exists(localPath))
ret |= await _DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}"); ret |= await _DownloadImage(image, localPath, $"The image n°{id} of {name}");
}
if (show.Logo != null)
{
string logoPath = await GetLogo(show);
if (alwaysDownload || !await _files.Exists(logoPath))
ret |= await _DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}");
}
if (show.Backdrop != null)
{
string backdropPath = await GetThumbnail(show);
if (alwaysDownload || !await _files.Exists(backdropPath))
ret |= await _DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}");
} }
return ret; return ret;
} }
/// <summary> /// <summary>
/// Download images of a specified person. /// Retrieve the local path of an image of the given item <b>without an extension</b>.
/// </summary> /// </summary>
/// <param name="people"> /// <param name="item">The item to retrieve the poster from.</param>
/// The item to cache images. /// <param name="imageID">The ID of the image. See <see cref="Images"/> for values.</param>
/// </param> /// <typeparam name="T">The type of the item</typeparam>
/// <param name="alwaysDownload"> /// <returns>The path of the image for the given resource, <b>even if it does not exists</b></returns>
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise. private async Task<string> _GetPrivateImagePath<T>(T item, int imageID)
/// </param>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _Validate([NotNull] People people, bool alwaysDownload)
{
if (people == null)
throw new ArgumentNullException(nameof(people));
if (people.Poster == null)
return false;
string localPath = await GetPoster(people);
if (alwaysDownload || !await _files.Exists(localPath))
return await _DownloadImage(people.Poster, localPath, $"The profile picture of {people.Name}");
return false;
}
/// <summary>
/// Download images of a specified season.
/// </summary>
/// <param name="season">
/// 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>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _Validate([NotNull] Season season, bool alwaysDownload)
{
if (season.Poster == null)
return false;
string localPath = await GetPoster(season);
if (alwaysDownload || !await _files.Exists(localPath))
return await _DownloadImage(season.Poster, localPath, $"The poster of {season.Slug}");
return false;
}
/// <summary>
/// Download images of a specified episode.
/// </summary>
/// <param name="episode">
/// 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>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _Validate([NotNull] Episode episode, bool alwaysDownload)
{
if (episode.Thumb == null)
return false;
string localPath = await _GetEpisodeThumb(episode);
if (alwaysDownload || !await _files.Exists(localPath))
return await _DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}");
return false;
}
/// <summary>
/// Download images of a specified provider.
/// </summary>
/// <param name="provider">
/// 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>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _Validate([NotNull] Provider provider, bool alwaysDownload)
{
if (provider.Logo == null)
return false;
string localPath = await GetLogo(provider);
if (alwaysDownload || !await _files.Exists(localPath))
return await _DownloadImage(provider.Logo, localPath, $"The logo of {provider.Slug}");
return false;
}
/// <inheritdoc />
public Task<string> GetPoster<T>(T item)
where T : IResource
{ {
if (item == null) if (item == null)
throw new ArgumentNullException(nameof(item)); throw new ArgumentNullException(nameof(item));
return item switch
string directory = await _files.GetExtraDirectory(item);
string imageName = imageID switch
{ {
Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "poster.jpg")), Images.Poster => "poster",
Season season => _GetSeasonPoster(season), Images.Logo => "logo",
People actor => Task.FromResult(_files.Combine(_options.CurrentValue.PeoplePath, $"{actor.Slug}.jpg")), Images.Thumbnail => "thumbnail",
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a poster.") Images.Trailer => "trailer",
_ => $"{imageID}"
}; };
switch (item)
{
case Season season:
imageName = $"season-{season.SeasonNumber}-{imageName}";
break;
case Episode episode:
directory = await _files.CreateDirectory(_files.Combine(directory, "Thumbnails"));
imageName = $"{Path.GetFileNameWithoutExtension(episode.Path)}-{imageName}";
break;
} }
/// <summary> return _files.Combine(directory, imageName);
/// Retrieve the path of a season's poster.
/// </summary>
/// <param name="season">The season to retrieve the poster from.</param>
/// <returns>The path of the season's poster.</returns>
private async Task<string> _GetSeasonPoster(Season season)
{
if (season.Show == null)
await _library.Value.Load(season, x => x.Show);
return _files.Combine(_files.GetExtraDirectory(season.Show), $"season-{season.SeasonNumber}.jpg");
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<string> GetThumbnail<T>(T item) public async Task<string> GetImagePath<T>(T item, int imageID)
where T : IResource where T : IThumbnails
{ {
if (item == null) string basePath = await _GetPrivateImagePath(item, imageID);
throw new ArgumentNullException(nameof(item)); string directory = Path.GetDirectoryName(basePath);
return item switch string baseFile = Path.GetFileName(basePath);
{ return (await _files.ListFiles(directory!))
Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "backdrop.jpg")), .FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == baseFile);
Episode episode => _GetEpisodeThumb(episode),
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a thumbnail.")
};
}
/// <summary>
/// Get the path for an episode's thumbnail.
/// </summary>
/// <param name="episode">The episode to retrieve the thumbnail from</param>
/// <returns>The path of the given episode's thumbnail.</returns>
private async Task<string> _GetEpisodeThumb(Episode episode)
{
if (episode.Show == null)
await _library.Value.Load(episode, x => x.Show);
string dir = _files.Combine(_files.GetExtraDirectory(episode.Show), "Thumbnails");
await _files.CreateDirectory(dir);
return _files.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg");
}
/// <inheritdoc />
public Task<string> GetLogo<T>(T item)
where T : IResource
{
if (item == null)
throw new ArgumentNullException(nameof(item));
return Task.FromResult(item switch
{
Show show => _files.Combine(_files.GetExtraDirectory(show), "logo.png"),
Provider provider => _files.Combine(_options.CurrentValue.ProviderPath,
$"{provider.Slug}.{provider.LogoExtension}"),
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a thumbnail.")
});
} }
} }
} }

View File

@ -88,10 +88,8 @@ namespace Kyoo.Controllers
public async Task<Track[]> ExtractInfos(Episode episode, bool reextract) public async Task<Track[]> ExtractInfos(Episode episode, bool reextract)
{ {
if (episode.Show == null)
await _library.Value.Load(episode, x => x.Show); await _library.Value.Load(episode, x => x.Show);
string dir = await _files.GetExtraDirectory(episode.Show);
string dir = _files.GetExtraDirectory(episode.Show);
if (dir == null) if (dir == null)
throw new ArgumentException("Invalid path."); throw new ArgumentException("Invalid path.");
return await Task.Factory.StartNew( return await Task.Factory.StartNew(

View File

@ -101,13 +101,13 @@ namespace Kyoo
/// <inheritdoc /> /// <inheritdoc />
public void Configure(ContainerBuilder builder) public void Configure(ContainerBuilder builder)
{ {
builder.RegisterComposite<FileSystemComposite, IFileSystem>(); builder.RegisterComposite<FileSystemComposite, IFileSystem>().InstancePerLifetimeScope();
builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance(); builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance();
builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance(); builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance();
builder.RegisterType<ConfigurationManager>().As<IConfigurationManager>().SingleInstance(); builder.RegisterType<ConfigurationManager>().As<IConfigurationManager>().SingleInstance();
builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance(); builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance();
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().SingleInstance(); builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
builder.RegisterType<TaskManager>().As<ITaskManager>().SingleInstance(); builder.RegisterType<TaskManager>().As<ITaskManager>().SingleInstance();
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope(); builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance(); builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance();

View File

@ -40,7 +40,7 @@
<PackageReference Include="Autofac.Extras.AttributeMetadata" Version="6.0.0" /> <PackageReference Include="Autofac.Extras.AttributeMetadata" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.17" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="5.0.0-preview.8.20414.8" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.8" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>

View File

@ -25,16 +25,6 @@ namespace Kyoo.Models.Options
/// </summary> /// </summary>
public string PluginPath { get; set; } = "plugins/"; public string PluginPath { get; set; } = "plugins/";
/// <summary>
/// The path of the people pictures.
/// </summary>
public string PeoplePath { get; set; } = "people/";
/// <summary>
/// The path of providers icons.
/// </summary>
public string ProviderPath { get; set; } = "providers/";
/// <summary> /// <summary>
/// The temporary folder to cache transmuxed file. /// The temporary folder to cache transmuxed file.
/// </summary> /// </summary>
@ -44,5 +34,22 @@ namespace Kyoo.Models.Options
/// The temporary folder to cache transcoded file. /// The temporary folder to cache transcoded file.
/// </summary> /// </summary>
public string TranscodePath { get; set; } = "cached/transcode"; public string TranscodePath { get; set; } = "cached/transcode";
/// <summary>
/// <c>true</c> if the metadata of a show/season/episode should be stored in the same directory as video files,
/// <c>false</c> to save them in a kyoo specific directory.
/// </summary>
/// <remarks>
/// Some file systems might discard this option to store them somewhere else.
/// For example, readonly file systems will probably store them in a kyoo specific directory.
/// </remarks>
public bool MetadataInShow { get; set; } = true;
/// <summary>
/// The path for metadata if they are not stored near show (see <see cref="MetadataInShow"/>).
/// Some resources can't be stored near a show and they are stored in this directory
/// (like <see cref="Provider"/>).
/// </summary>
public string MetadataPath { get; set; } = "metadata/";
} }
} }

View File

@ -116,16 +116,11 @@ namespace Kyoo.Tasks
else else
show = registeredShow; show = registeredShow;
// If they are not already loaded, load external ids to allow metadata providers to use them.
if (show.ExternalIDs == null)
await _libraryManager.Load(show, x => x.ExternalIDs);
progress.Report(50); progress.Report(50);
if (season != null) if (season != null)
season.Show = show; season.Show = show;
season = await _RegisterAndFill(season); season = await _RegisterAndFill(season);
if (season != null)
season.Title ??= $"Season {season.SeasonNumber}";
progress.Report(60); progress.Report(60);
episode.Show = show; episode.Show = show;
@ -163,16 +158,32 @@ namespace Kyoo.Tasks
/// <typeparam name="T">The type of the item</typeparam> /// <typeparam name="T">The type of the item</typeparam>
/// <returns>The existing or filled item.</returns> /// <returns>The existing or filled item.</returns>
private async Task<T> _RegisterAndFill<T>(T item) private async Task<T> _RegisterAndFill<T>(T item)
where T : class, IResource where T : class, IResource, IThumbnails, IMetadata
{ {
if (item == null || string.IsNullOrEmpty(item.Slug)) if (item == null || string.IsNullOrEmpty(item.Slug))
return null; return null;
T existing = await _libraryManager.GetOrDefault<T>(item.Slug); T existing = await _libraryManager.GetOrDefault<T>(item.Slug);
if (existing != null) if (existing != null)
{
await _libraryManager.Load(existing, x => x.ExternalIDs);
return existing; return existing;
}
item = await _metadataProvider.Get(item); item = await _metadataProvider.Get(item);
await _thumbnailsManager.DownloadImages(item); await _thumbnailsManager.DownloadImages(item);
switch (item)
{
case Show show when show.People != null:
foreach (PeopleRole role in show.People)
await _thumbnailsManager.DownloadImages(role.People);
break;
case Season season:
season.Title ??= $"Season {season.SeasonNumber}";
break;
}
return await _libraryManager.CreateIfNotExists(item); return await _libraryManager.CreateIfNotExists(item);
} }
} }

View File

@ -6,6 +6,7 @@ using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Models.Exceptions;
using Kyoo.Models.Options; using Kyoo.Models.Options;
using Kyoo.Models.Permissions; using Kyoo.Models.Permissions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -19,11 +20,18 @@ namespace Kyoo.Api
public class CollectionApi : CrudApi<Collection> public class CollectionApi : CrudApi<Collection>
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _files;
private readonly IThumbnailsManager _thumbs;
public CollectionApi(ILibraryManager libraryManager, IOptions<BasicOptions> options) public CollectionApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs,
IOptions<BasicOptions> options)
: base(libraryManager.CollectionRepository, options.Value.PublicUrl) : base(libraryManager.CollectionRepository, options.Value.PublicUrl)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_files = files;
_thumbs = thumbs;
} }
[HttpGet("{id:int}/show")] [HttpGet("{id:int}/show")]
@ -129,5 +137,48 @@ namespace Kyoo.Api
return BadRequest(new {Error = ex.Message}); return BadRequest(new {Error = ex.Message});
} }
} }
[HttpGet("{slug}/poster")]
public async Task<IActionResult> GetPoster(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Poster));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/logo")]
public async Task<IActionResult> GetLogo(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Logo));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpGet("{slug}/backdrop")]
[HttpGet("{slug}/thumbnail")]
public async Task<IActionResult> GetBackdrop(string slug)
{
try
{
Collection collection = await _libraryManager.Get<Collection>(slug);
return _files.FileResult(await _thumbs.GetImagePath(collection, Images.Thumbnail));
}
catch (ItemNotFoundException)
{
return NotFound();
}
}
} }
} }

View File

@ -195,7 +195,7 @@ namespace Kyoo.Api
try try
{ {
Episode episode = await _libraryManager.Get<Episode>(id); Episode episode = await _libraryManager.Get<Episode>(id);
return _files.FileResult(await _thumbnails.GetThumbnail(episode)); return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail));
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
@ -210,7 +210,7 @@ namespace Kyoo.Api
try try
{ {
Episode episode = await _libraryManager.Get<Episode>(slug); Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(await _thumbnails.GetThumbnail(episode)); return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail));
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {

View File

@ -94,7 +94,7 @@ namespace Kyoo.Api
People people = await _libraryManager.GetOrDefault<People>(id); People people = await _libraryManager.GetOrDefault<People>(id);
if (people == null) if (people == null)
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetPoster(people)); return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster));
} }
[HttpGet("{slug}/poster")] [HttpGet("{slug}/poster")]
@ -103,7 +103,7 @@ namespace Kyoo.Api
People people = await _libraryManager.GetOrDefault<People>(slug); People people = await _libraryManager.GetOrDefault<People>(slug);
if (people == null) if (people == null)
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetPoster(people)); return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster));
} }
} }
} }

View File

@ -36,7 +36,7 @@ namespace Kyoo.Api
Provider provider = await _libraryManager.GetOrDefault<Provider>(id); Provider provider = await _libraryManager.GetOrDefault<Provider>(id);
if (provider == null) if (provider == null)
return NotFound(); return NotFound();
return _files.FileResult(await _thumbnails.GetLogo(provider)); return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo));
} }
[HttpGet("{slug}/logo")] [HttpGet("{slug}/logo")]
@ -45,7 +45,7 @@ namespace Kyoo.Api
Provider provider = await _libraryManager.GetOrDefault<Provider>(slug); Provider provider = await _libraryManager.GetOrDefault<Provider>(slug);
if (provider == null) if (provider == null)
return NotFound(); return NotFound();
return _files.FileResult(await _thumbnails.GetLogo(provider)); return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo));
} }
} }
} }

View File

@ -151,7 +151,7 @@ namespace Kyoo.Api
if (season == null) if (season == null)
return NotFound(); return NotFound();
await _libraryManager.Load(season, x => x.Show); await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetPoster(season)); return _files.FileResult(await _thumbs.GetImagePath(season, Images.Poster));
} }
[HttpGet("{slug}/poster")] [HttpGet("{slug}/poster")]
@ -161,7 +161,7 @@ namespace Kyoo.Api
if (season == null) if (season == null)
return NotFound(); return NotFound();
await _libraryManager.Load(season, x => x.Show); await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetPoster(season)); return _files.FileResult(await _thumbs.GetImagePath(season, Images.Poster));
} }
} }
} }

View File

@ -383,7 +383,7 @@ namespace Kyoo.Api
try try
{ {
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments"); string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments");
return (await _files.ListFiles(path)) return (await _files.ListFiles(path))
.ToDictionary(Path.GetFileNameWithoutExtension, .ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}api/shows/{slug}/fonts/{Path.GetFileName(x)}"); x => $"{BaseURL}api/shows/{slug}/fonts/{Path.GetFileName(x)}");
@ -402,7 +402,7 @@ namespace Kyoo.Api
try try
{ {
Show show = await _libraryManager.Get<Show>(showSlug); Show show = await _libraryManager.Get<Show>(showSlug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", slug);
return _files.FileResult(path); return _files.FileResult(path);
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
@ -417,7 +417,7 @@ namespace Kyoo.Api
try try
{ {
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetPoster(show)); return _files.FileResult(await _thumbs.GetImagePath(show, Images.Poster));
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
@ -431,7 +431,7 @@ namespace Kyoo.Api
try try
{ {
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetLogo(show)); return _files.FileResult(await _thumbs.GetImagePath(show, Images.Logo));
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
@ -446,7 +446,7 @@ namespace Kyoo.Api
try try
{ {
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetThumbnail(show)); return _files.FileResult(await _thumbs.GetImagePath(show, Images.Thumbnail));
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {

View File

@ -3,10 +3,10 @@
"url": "http://*:5000", "url": "http://*:5000",
"publicUrl": "http://localhost:5000/", "publicUrl": "http://localhost:5000/",
"pluginsPath": "plugins/", "pluginsPath": "plugins/",
"peoplePath": "people/",
"providerPath": "providers/",
"transmuxPath": "cached/transmux", "transmuxPath": "cached/transmux",
"transcodePath": "cached/transcode" "transcodePath": "cached/transcode",
"metadataInShow": true,
"metadataPath": "metadata/"
}, },
"database": { "database": {
@ -70,5 +70,8 @@
"tvdb": { "tvdb": {
"apiKey": "REDACTED" "apiKey": "REDACTED"
},
"the-moviedb": {
"apiKey": "REDACTED"
} }
} }

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Xunit.Abstractions;
namespace Kyoo.Tests
{
public class RepositoryActivator : IDisposable, IAsyncDisposable
{
public TestContext Context { get; }
public ILibraryManager LibraryManager { get; }
private readonly List<DatabaseContext> _databases = new();
public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null)
{
Context = postgres == null
? new SqLiteTestContext(output)
: new PostgresTestContext(postgres, output);
ProviderRepository provider = new(_NewContext());
LibraryRepository library = new(_NewContext(), provider);
CollectionRepository collection = new(_NewContext(), provider);
GenreRepository genre = new(_NewContext());
StudioRepository studio = new(_NewContext(), provider);
PeopleRepository people = new(_NewContext(), provider,
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository));
ShowRepository show = new(_NewContext(), studio, people, genre, provider);
SeasonRepository season = new(_NewContext(), provider);
LibraryItemRepository libraryItem = new(_NewContext(),
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
TrackRepository track = new(_NewContext());
EpisodeRepository episode = new(_NewContext(), provider, track);
UserRepository user = new(_NewContext());
LibraryManager = new LibraryManager(new IBaseRepository[] {
provider,
library,
libraryItem,
collection,
show,
season,
episode,
track,
people,
studio,
genre,
user
});
}
private DatabaseContext _NewContext()
{
DatabaseContext context = Context.New();
_databases.Add(context);
return context;
}
public void Dispose()
{
foreach (DatabaseContext context in _databases)
context.Dispose();
Context.Dispose();
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
foreach (DatabaseContext context in _databases)
await context.DisposeAsync();
await Context.DisposeAsync();
}
}
}

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class CollectionTests : ACollectionTests
{
public CollectionTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class CollectionTests : ACollectionTests
{
public CollectionTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ACollectionTests : RepositoryTests<Collection>
{
private readonly ICollectionRepository _repository;
protected ACollectionTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.CollectionRepository;
}
[Fact]
public async Task CreateWithEmptySlugTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.Slug = "";
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(collection));
}
[Fact]
public async Task CreateWithNumberSlugTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.Slug = "2";
Collection ret = await _repository.Create(collection);
Assert.Equal("2!", ret.Slug);
}
[Fact]
public async Task CreateWithoutNameTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.Name = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(collection));
}
[Fact]
public async Task CreateWithExternalIdTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.ExternalIDs = new[]
{
new MetadataID
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataID = "new-id"
}
};
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());
}
[Fact]
public async Task EditTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.Name = "New Title";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
[Fact]
public async Task EditMetadataTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalIDs = new[]
{
new MetadataID
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
[Fact]
public async Task AddMetadataTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalIDs = new List<MetadataID>
{
new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
};
await _repository.Edit(value, false);
{
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataID = "id"
});
await _repository.Edit(value, false);
{
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
}
[Theory]
[InlineData("test")]
[InlineData("super")]
[InlineData("title")]
[InlineData("TiTlE")]
[InlineData("SuPeR")]
public async Task SearchTest(string query)
{
Collection value = new()
{
Slug = "super-test",
Name = "This is a test title",
};
await _repository.Create(value);
ICollection<Collection> ret = await _repository.Search(query);
KAssert.DeepEqual(value, ret.First());
}
}
}

View File

@ -1,6 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -59,7 +62,8 @@ namespace Kyoo.Tests.Database
episode = await _repository.Edit(new Episode episode = await _repository.Edit(new Episode
{ {
ID = 1, ID = 1,
SeasonNumber = 2 SeasonNumber = 2,
ShowID = 1
}, false); }, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
episode = await _repository.Get(1); episode = await _repository.Get(1);
@ -74,7 +78,8 @@ namespace Kyoo.Tests.Database
episode = await _repository.Edit(new Episode episode = await _repository.Edit(new Episode
{ {
ID = 1, ID = 1,
EpisodeNumber = 2 EpisodeNumber = 2,
ShowID = 1
}, false); }, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(1); episode = await _repository.Get(1);
@ -93,10 +98,6 @@ namespace Kyoo.Tests.Database
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug);
} }
// TODO absolute numbering tests
[Fact] [Fact]
public void AbsoluteSlugTest() public void AbsoluteSlugTest()
{ {
@ -133,7 +134,8 @@ namespace Kyoo.Tests.Database
Episode episode = await _repository.Edit(new Episode Episode episode = await _repository.Edit(new Episode
{ {
ID = 2, ID = 2,
AbsoluteNumber = 56 AbsoluteNumber = 56,
ShowID = 1
}, false); }, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
episode = await _repository.Get(2); episode = await _repository.Get(2);
@ -148,7 +150,8 @@ namespace Kyoo.Tests.Database
{ {
ID = 2, ID = 2,
SeasonNumber = 1, SeasonNumber = 1,
EpisodeNumber = 2 EpisodeNumber = 2,
ShowID = 1
}, false); }, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(2); episode = await _repository.Get(2);
@ -188,5 +191,137 @@ namespace Kyoo.Tests.Database
Episode episode = await _repository.Get(3); Episode episode = await _repository.Get(3);
Assert.Equal("john-wick", episode.Slug); Assert.Equal("john-wick", episode.Slug);
} }
[Fact]
public async Task CreateWithExternalIdTest()
{
Episode value = TestSample.GetNew<Episode>();
value.ExternalIDs = new[]
{
new MetadataID
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataID = "new-id"
}
};
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());
}
[Fact]
public async Task EditTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.Title = "New Title";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
[Fact]
public async Task EditMetadataTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalIDs = new[]
{
new MetadataID
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
[Fact]
public async Task AddMetadataTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalIDs = new List<MetadataID>
{
new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
};
await _repository.Edit(value, false);
{
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataID = "id"
});
await _repository.Edit(value, false);
{
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
}
[Theory]
[InlineData("test")]
[InlineData("super")]
[InlineData("title")]
[InlineData("TiTlE")]
[InlineData("SuPeR")]
public async Task SearchTest(string query)
{
Episode value = new()
{
Title = "This is a test super title",
ShowID = 1,
AbsoluteNumber = 2
};
await _repository.Create(value);
ICollection<Episode> ret = await _repository.Search(query);
KAssert.DeepEqual(value, ret.First());
}
} }
} }

View File

@ -55,7 +55,7 @@ namespace Kyoo.Tests.Database
[Fact] [Fact]
public async Task GetCollectionTests() public async Task GetCollectionTests()
{ {
LibraryItem expected = new(TestSample.Get<Show>()); LibraryItem expected = new(TestSample.Get<Collection>());
LibraryItem actual = await _repository.Get(-1); LibraryItem actual = await _repository.Get(-1);
KAssert.DeepEqual(expected, actual); KAssert.DeepEqual(expected, actual);
} }
@ -79,9 +79,10 @@ namespace Kyoo.Tests.Database
[Fact] [Fact]
public async Task GetDuplicatedSlugTests() public async Task GetDuplicatedSlugTests()
{ {
await _repositories.LibraryManager.Create(new Collection() await _repositories.LibraryManager.Create(new Collection
{ {
Slug = TestSample.Get<Show>().Slug Slug = TestSample.Get<Show>().Slug,
Name = "name"
}); });
await Assert.ThrowsAsync<InvalidOperationException>(() => _repository.Get(TestSample.Get<Show>().Slug)); await Assert.ThrowsAsync<InvalidOperationException>(() => _repository.Get(TestSample.Get<Show>().Slug));
} }

View File

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class LibraryTests : ALibraryTests
{
public LibraryTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class LibraryTests : ALibraryTests
{
public LibraryTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ALibraryTests : RepositoryTests<Library>
{
private readonly ILibraryRepository _repository;
protected ALibraryTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.LibraryRepository;
}
[Fact]
public async Task CreateWithoutPathTest()
{
Library library = TestSample.GetNew<Library>();
library.Paths = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(library));
}
[Fact]
public async Task CreateWithEmptySlugTest()
{
Library library = TestSample.GetNew<Library>();
library.Slug = "";
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(library));
}
[Fact]
public async Task CreateWithNumberSlugTest()
{
Library library = TestSample.GetNew<Library>();
library.Slug = "2";
Library ret = await _repository.Create(library);
Assert.Equal("2!", ret.Slug);
}
[Fact]
public async Task CreateWithoutNameTest()
{
Library library = TestSample.GetNew<Library>();
library.Name = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(library));
}
[Fact]
public async Task CreateWithProvider()
{
Library library = TestSample.GetNew<Library>();
library.Providers = new[] { TestSample.Get<Provider>() };
await _repository.Create(library);
Library retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.Providers);
Assert.Equal(1, retrieved.Providers.Count);
Assert.Equal(TestSample.Get<Provider>().Slug, retrieved.Providers.First().Slug);
}
[Fact]
public async Task EditTest()
{
Library value = await _repository.Get(TestSample.Get<Library>().Slug);
value.Paths = new [] {"/super", "/test"};
value.Name = "New Title";
Library edited = await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Library show = await database.Libraries.FirstAsync();
KAssert.DeepEqual(show, edited);
}
[Fact]
public async Task EditProvidersTest()
{
Library value = await _repository.Get(TestSample.Get<Library>().Slug);
value.Providers = new[]
{
TestSample.GetNew<Provider>()
};
Library edited = await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Library show = await database.Libraries
.Include(x => x.Providers)
.FirstAsync();
show.Providers.ForEach(x => x.Libraries = null);
edited.Providers.ForEach(x => x.Libraries = null);
KAssert.DeepEqual(show, edited);
}
[Fact]
public async Task AddProvidersTest()
{
Library value = await _repository.Get(TestSample.Get<Library>().Slug);
await Repositories.LibraryManager.Load(value, x => x.Providers);
value.Providers.Add(TestSample.GetNew<Provider>());
Library edited = await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Library show = await database.Libraries
.Include(x => x.Providers)
.FirstAsync();
show.Providers.ForEach(x => x.Libraries = null);
edited.Providers.ForEach(x => x.Libraries = null);
KAssert.DeepEqual(show, edited);
}
[Theory]
[InlineData("test")]
[InlineData("super")]
[InlineData("title")]
[InlineData("TiTlE")]
[InlineData("SuPeR")]
public async Task SearchTest(string query)
{
Library value = new()
{
Slug = "super-test",
Name = "This is a test title",
Paths = new [] {"path"}
};
await _repository.Create(value);
ICollection<Library> ret = await _repository.Search(query);
KAssert.DeepEqual(value, ret.First());
}
}
}

View File

@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace SqLite
{
public class PeopleTests : APeopleTests
{
public PeopleTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class PeopleTests : APeopleTests
{
public PeopleTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class APeopleTests : RepositoryTests<People>
{
private readonly IPeopleRepository _repository;
protected APeopleTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.PeopleRepository;
}
[Fact]
public async Task CreateWithExternalIdTest()
{
People value = TestSample.GetNew<People>();
value.ExternalIDs = new[]
{
new MetadataID
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataID = "new-id"
}
};
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());
}
[Fact]
public async Task EditTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.Name = "New Name";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
[Fact]
public async Task EditMetadataTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.ExternalIDs = new[]
{
new MetadataID
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
[Fact]
public async Task AddMetadataTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.ExternalIDs = new List<MetadataID>
{
new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataID = "id"
},
};
await _repository.Edit(value, false);
{
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalIDs.Add(new MetadataID
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataID = "id"
});
await _repository.Edit(value, false);
{
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
}
[Theory]
[InlineData("Me")]
[InlineData("me")]
[InlineData("na")]
public async Task SearchTest(string query)
{
People value = new()
{
Slug = "slug",
Name = "name",
};
await _repository.Create(value);
ICollection<People> ret = await _repository.Search(query);
KAssert.DeepEqual(value, ret.First());
}
}
}

View File

@ -16,13 +16,6 @@ namespace Kyoo.Tests.Database
_repositories = new RepositoryActivator(output); _repositories = new RepositoryActivator(output);
} }
[Fact]
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
public void SampleTest()
{
Assert.False(ReferenceEquals(TestSample.Get<Show>(), TestSample.Get<Show>()));
}
public void Dispose() public void Dispose()
{ {
_repositories.Dispose(); _repositories.Dispose();
@ -33,5 +26,12 @@ namespace Kyoo.Tests.Database
{ {
return _repositories.DisposeAsync(); return _repositories.DisposeAsync();
} }
[Fact]
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
public void SampleTest()
{
Assert.False(ReferenceEquals(TestSample.Get<Show>(), TestSample.Get<Show>()));
}
} }
} }

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