mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Merge pull request #32 from AnonymusRaccoon/the-moviedb
Adding TheMovieDB provider
This commit is contained in:
commit
387bf4c34d
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
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
|
||||
run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover'
|
||||
env:
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
- name: Sanitize coverage output
|
||||
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
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
|
@ -2,11 +2,25 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
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>
|
||||
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
|
||||
/// </summary>
|
||||
@ -43,6 +57,16 @@ namespace Kyoo.Controllers
|
||||
/// <returns>A reader to read the file.</returns>
|
||||
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>
|
||||
/// Create a new file at <paramref name="path"></paramref>.
|
||||
/// </summary>
|
||||
@ -81,12 +105,13 @@ namespace Kyoo.Controllers
|
||||
public Task<bool> Exists([NotNull] string path);
|
||||
|
||||
/// <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.
|
||||
/// It can be useful if the filesystem is readonly.
|
||||
/// </summary>
|
||||
/// <param name="show">The show to proceed</param>
|
||||
/// <returns>The extra directory of the show</returns>
|
||||
public string GetExtraDirectory([NotNull] Show show);
|
||||
/// <param name="resource">The resource to proceed</param>
|
||||
/// <typeparam name="T">The type of the resource.</typeparam>
|
||||
/// <returns>The extra directory of the resource.</returns>
|
||||
public Task<string> GetExtraDirectory<T>([NotNull] T resource);
|
||||
}
|
||||
}
|
@ -225,42 +225,51 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="obj">The source object.</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="T2">The related resource's type</typeparam>
|
||||
/// <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}(T, System.String)"/>
|
||||
/// <seealso cref="Load(IResource, string)"/>
|
||||
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
|
||||
/// <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)"/>
|
||||
/// <seealso cref="Load(IResource, string, bool)"/>
|
||||
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member, bool force = false)
|
||||
where T : class, IResource
|
||||
where T2 : class, IResource, new();
|
||||
where T2 : class, IResource;
|
||||
|
||||
/// <summary>
|
||||
/// Load a collection of related resource
|
||||
/// </summary>
|
||||
/// <param name="obj">The source object.</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="T2">The related resource's type</typeparam>
|
||||
/// <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}(T, System.String)"/>
|
||||
/// <seealso cref="Load(IResource, string)"/>
|
||||
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
|
||||
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
|
||||
/// <seealso cref="Load{T}(T, System.String, bool)"/>
|
||||
/// <seealso cref="Load(IResource, string, bool)"/>
|
||||
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
|
||||
where T : class, IResource
|
||||
where T2 : class, new();
|
||||
where T2 : class;
|
||||
|
||||
/// <summary>
|
||||
/// Load a related resource by it's name
|
||||
/// </summary>
|
||||
/// <param name="obj">The source object.</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>
|
||||
/// <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,System.Collections.Generic.ICollection{T2}}})"/>
|
||||
/// <seealso cref="Load(IResource, string)"/>
|
||||
Task<T> Load<T>([NotNull] T 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(IResource, string, bool)"/>
|
||||
Task<T> Load<T>([NotNull] T obj, string memberName, bool force = false)
|
||||
where T : class, IResource;
|
||||
|
||||
/// <summary>
|
||||
@ -268,10 +277,13 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="obj">The source object.</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}})"/>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
|
||||
/// <seealso cref="Load{T}(T, System.String)"/>
|
||||
Task Load([NotNull] IResource obj, string memberName);
|
||||
/// <param name="force">
|
||||
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
|
||||
/// </param>
|
||||
/// <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>
|
||||
/// Get items (A wrapper arround shows or collections) from a library.
|
||||
|
@ -590,10 +590,10 @@ namespace Kyoo.Controllers
|
||||
/// <param name="limit">Pagination information (where to start and how many to get)</param>
|
||||
/// <typeparam name="T">The type of metadata to retrieve</typeparam>
|
||||
/// <returns>A filtered list of external ids.</returns>
|
||||
Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
|
||||
Sort<MetadataID<T>> sort = default,
|
||||
Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
|
||||
Sort<MetadataID> sort = default,
|
||||
Pagination limit = default)
|
||||
where T : class, IResource;
|
||||
where T : class, IMetadata;
|
||||
|
||||
/// <summary>
|
||||
/// 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="limit">Pagination information (where to start and how many to get)</param>
|
||||
/// <returns>A filtered list of external ids.</returns>
|
||||
Task<ICollection<MetadataID<T>>> GetMetadataID<T>([Optional] Expression<Func<MetadataID<T>, bool>> where,
|
||||
Expression<Func<MetadataID<T>, object>> sort,
|
||||
Task<ICollection<MetadataID>> GetMetadataID<T>([Optional] Expression<Func<MetadataID, bool>> where,
|
||||
Expression<Func<MetadataID, object>> sort,
|
||||
Pagination limit = default
|
||||
) where T : class, IResource
|
||||
=> GetMetadataID(where, new Sort<MetadataID<T>>(sort), limit);
|
||||
) where T : class, IMetadata
|
||||
=> GetMetadataID<T>(where, new Sort<MetadataID>(sort), limit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@ -23,37 +22,17 @@ namespace Kyoo.Controllers
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||
Task<bool> DownloadImages<T>([NotNull] T item, bool alwaysDownload = false)
|
||||
where T : IResource;
|
||||
where T : IThumbnails;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the local path of the poster of the given item.
|
||||
/// Retrieve the local path of an image of the given item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to retrieve the poster from.</param>
|
||||
/// <param name="imageID">The ID of the image. See <see cref="Images"/> for values.</param>
|
||||
/// <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 poster for the given resource (it might or might not exists).</returns>
|
||||
Task<string> GetPoster<T>([NotNull] T item)
|
||||
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;
|
||||
/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
|
||||
Task<string> GetImagePath<T>([NotNull] T item, int imageID)
|
||||
where T : IThumbnails;
|
||||
}
|
||||
}
|
||||
|
@ -162,34 +162,6 @@ namespace Kyoo.Controllers
|
||||
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>
|
||||
/// Set relations between to objects.
|
||||
/// </summary>
|
||||
@ -211,11 +183,46 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <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)
|
||||
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
|
||||
{
|
||||
(Library l, nameof(Library.Providers)) => ProviderRepository
|
||||
@ -231,7 +238,12 @@ namespace Kyoo.Controllers
|
||||
.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))
|
||||
.Then(x => c.Shows = x),
|
||||
|
||||
@ -241,9 +253,9 @@ namespace Kyoo.Controllers
|
||||
|
||||
|
||||
(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.First = y; x.FirstID = y.ID; }),
|
||||
(x, y) => { x.ResourceID = y.ID; }),
|
||||
|
||||
(Show s, nameof(Show.Genres)) => GenreRepository
|
||||
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
|
||||
@ -281,9 +293,9 @@ namespace Kyoo.Controllers
|
||||
|
||||
|
||||
(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.First = y; x.FirstID = y.ID; }),
|
||||
(x, y) => { x.ResourceID = y.ID; }),
|
||||
|
||||
(Season s, nameof(Season.Episodes)) => SetRelation(s,
|
||||
EpisodeRepository.GetAll(x => x.Season.ID == obj.ID),
|
||||
@ -300,9 +312,9 @@ namespace Kyoo.Controllers
|
||||
|
||||
|
||||
(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.First = y; x.FirstID = y.ID; }),
|
||||
(x, y) => { x.ResourceID = y.ID; }),
|
||||
|
||||
(Episode e, nameof(Episode.Tracks)) => SetRelation(e,
|
||||
TrackRepository.GetAll(x => x.Episode.ID == obj.ID),
|
||||
@ -344,11 +356,16 @@ namespace Kyoo.Controllers
|
||||
.GetAll(x => x.Studio.ID == obj.ID)
|
||||
.Then(x => s.Shows = x),
|
||||
|
||||
(Studio s, nameof(Studio.ExternalIDs)) => SetRelation(s,
|
||||
ProviderRepository.GetMetadataID<Studio>(x => x.ResourceID == obj.ID),
|
||||
(x, y) => x.ExternalIDs = y,
|
||||
(x, y) => { x.ResourceID = y.ID; }),
|
||||
|
||||
|
||||
(People p, nameof(People.ExternalIDs)) => SetRelation(p,
|
||||
ProviderRepository.GetMetadataID<People>(x => x.FirstID == obj.ID),
|
||||
ProviderRepository.GetMetadataID<People>(x => x.ResourceID == obj.ID),
|
||||
(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
|
||||
.GetFromPeople(obj.ID)
|
||||
|
@ -16,13 +16,11 @@
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<LangVersion>default</LangVersion>
|
||||
|
||||
<DefineConstants>ENABLE_INTERNAL_LINKS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
|
@ -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 { }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
@ -18,7 +19,7 @@ namespace Kyoo.Models
|
||||
/// A type union between <see cref="Show"/> and <see cref="Collection"/>.
|
||||
/// This is used to list content put inside a library.
|
||||
/// </summary>
|
||||
public class LibraryItem : IResource
|
||||
public class LibraryItem : IResource, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -53,12 +54,16 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public DateTime? EndAir { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this item's poster.
|
||||
/// By default, the http path for this poster is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The type of this item (ether a collection, a show or a movie).
|
||||
@ -84,7 +89,7 @@ namespace Kyoo.Models
|
||||
Status = show.Status;
|
||||
StartAir = show.StartAir;
|
||||
EndAir = show.EndAir;
|
||||
Poster = show.Poster;
|
||||
Images = show.Images;
|
||||
Type = show.IsMovie ? ItemType.Movie : ItemType.Show;
|
||||
}
|
||||
|
||||
@ -101,7 +106,7 @@ namespace Kyoo.Models
|
||||
Status = Models.Status.Unknown;
|
||||
StartAir = null;
|
||||
EndAir = null;
|
||||
Poster = collection.Poster;
|
||||
Images = collection.Images;
|
||||
Type = ItemType.Collection;
|
||||
}
|
||||
|
||||
@ -117,7 +122,7 @@ namespace Kyoo.Models
|
||||
Status = x.Status,
|
||||
StartAir = x.StartAir,
|
||||
EndAir = x.EndAir,
|
||||
Poster= x.Poster,
|
||||
Images = x.Images,
|
||||
Type = x.IsMovie ? ItemType.Movie : ItemType.Show
|
||||
};
|
||||
|
||||
@ -133,7 +138,7 @@ namespace Kyoo.Models
|
||||
Status = Models.Status.Unknown,
|
||||
StartAir = null,
|
||||
EndAir = null,
|
||||
Poster = x.Poster,
|
||||
Images = x.Images,
|
||||
Type = ItemType.Collection
|
||||
};
|
||||
}
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,29 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// ID and link of an item on an external provider.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class MetadataID<T> : Link<T, Provider>
|
||||
where T : class, IResource
|
||||
public class MetadataID
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the resource which possess the metadata.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int ResourceID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the provider.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int ProviderID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The provider that can do something with this ID.
|
||||
/// </summary>
|
||||
public Provider Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the resource on the external provider.
|
||||
/// </summary>
|
||||
@ -20,21 +34,12 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
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>
|
||||
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
|
||||
/// </summary>
|
||||
public new static Expression<Func<MetadataID<T>, object>> PrimaryKey
|
||||
public static Expression<Func<MetadataID, object>> PrimaryKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return x => new {First = x.FirstID, Second = x.SecondID};
|
||||
}
|
||||
get { return x => new { First = x.ResourceID, Second = x.ProviderID }; }
|
||||
}
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@ namespace Kyoo.Models
|
||||
public string Slug => ForPeople ? Show.Slug : People.Slug;
|
||||
|
||||
/// <summary>
|
||||
/// Should this role be used as a Show substitute (the value is <c>false</c>) or
|
||||
/// as a People substitute (the value is <c>true</c>).
|
||||
/// Should this role be used as a Show substitute (the value is <c>true</c>) or
|
||||
/// as a People substitute (the value is <c>false</c>).
|
||||
/// </summary>
|
||||
public bool ForPeople { get; set; }
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
@ -8,7 +8,7 @@ namespace Kyoo.Models
|
||||
/// A class representing collections of <see cref="Show"/>.
|
||||
/// A collection can also be stored in a <see cref="Library"/>.
|
||||
/// </summary>
|
||||
public class Collection : IResource
|
||||
public class Collection : IResource, IMetadata, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -21,12 +21,17 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this poster.
|
||||
/// By default, the http path for this poster is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The description of this collection.
|
||||
@ -43,17 +48,7 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
|
||||
|
||||
#if ENABLE_INTERNAL_LINKS
|
||||
|
||||
/// <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
|
||||
/// <inheritdoc />
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// A class to represent a single show's episode.
|
||||
/// </summary>
|
||||
public class Episode : IResource
|
||||
public class Episode : IResource, IMetadata, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -74,9 +74,13 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int? SeasonID { get; set; }
|
||||
/// <summary>
|
||||
/// The season that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// This can be null if the season is unknown and the episode is only identified by it's <see cref="AbsoluteNumber"/>.
|
||||
/// The season that contains this episode.
|
||||
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// </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; }
|
||||
|
||||
/// <summary>
|
||||
@ -85,7 +89,7 @@ namespace Kyoo.Models
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of this episode is it's season.
|
||||
/// The number of this episode in it's season.
|
||||
/// </summary>
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
@ -99,12 +103,17 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this episode's thumbnail.
|
||||
/// By default, the http path for the thumbnail is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The title of this episode.
|
||||
@ -121,10 +130,8 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Episode>> ExternalIDs { get; set; }
|
||||
/// <inheritdoc />
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of tracks this episode has. This lists video, audio and subtitles available.
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
@ -25,13 +24,6 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[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>
|
||||
/// Create a new, empty <see cref="Genre"/>.
|
||||
/// </summary>
|
||||
|
79
Kyoo.Common/Models/Resources/Interfaces/IMetadata.cs
Normal file
79
Kyoo.Common/Models/Resources/Interfaces/IMetadata.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
49
Kyoo.Common/Models/Resources/Interfaces/IThumbnails.cs
Normal file
49
Kyoo.Common/Models/Resources/Interfaces/IThumbnails.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
@ -39,22 +38,5 @@ namespace Kyoo.Models
|
||||
/// The list of collections in this library.
|
||||
/// </summary>
|
||||
[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
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
@ -6,7 +7,7 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class People : IResource
|
||||
public class People : IResource, IMetadata, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -19,17 +20,20 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this poster.
|
||||
/// By default, the http path for this poster is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The link to metadata providers that this person has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<People>> ExternalIDs { get; set; }
|
||||
/// <inheritdoc />
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
@ -9,7 +9,7 @@ namespace Kyoo.Models
|
||||
/// This class contains metadata about <see cref="IMetadataProvider"/>.
|
||||
/// You can have providers even if you don't have the corresponding <see cref="IMetadataProvider"/>.
|
||||
/// </summary>
|
||||
public class Provider : IResource
|
||||
public class Provider : IResource, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -22,30 +22,23 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this provider's logo.
|
||||
/// By default, the http path for this logo is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The extension of the logo. This is used for http responses.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string LogoExtension { get; set; }
|
||||
[SerializeAs("{HOST}/api/providers/{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>
|
||||
/// The list of libraries that uses this provider.
|
||||
/// </summary>
|
||||
[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>
|
||||
/// Create a new, default, <see cref="Provider"/>
|
||||
/// </summary>
|
||||
@ -61,7 +54,10 @@ namespace Kyoo.Models
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
Logo = logo;
|
||||
Images = new Dictionary<int, string>
|
||||
{
|
||||
[Models.Images.Logo] = logo
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// A season of a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class Season : IResource
|
||||
public class Season : IResource, IMetadata, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -45,7 +45,8 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int ShowID { get; set; }
|
||||
/// <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>
|
||||
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
|
||||
|
||||
@ -74,17 +75,20 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this poster.
|
||||
/// By default, the http path for this poster is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Season>> ExternalIDs { get; set; }
|
||||
/// <inheritdoc />
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of episodes that this season contains.
|
||||
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
@ -11,7 +8,7 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// A series or a movie.
|
||||
/// </summary>
|
||||
public class Show : IResource, IOnMerge
|
||||
public class Show : IResource, IMetadata, IOnMerge, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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"/>.
|
||||
/// </summary>
|
||||
/// 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>
|
||||
/// The date this show started airing. It can be null if this is unknown.
|
||||
@ -63,43 +61,51 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public DateTime? EndAir { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this show's poster.
|
||||
/// By default, the http path for this poster is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The path of this show's logo.
|
||||
/// By default, the http path for this logo is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The path of this show's backdrop.
|
||||
/// By default, the http path for this backdrop is returned from the public API.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// True if this show represent a movie, false otherwise.
|
||||
/// </summary>
|
||||
public bool IsMovie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this show has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Show>> ExternalIDs { get; set; }
|
||||
/// <inheritdoc />
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Studio that made this show.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int? StudioID { get; set; }
|
||||
/// <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>
|
||||
[LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; }
|
||||
|
||||
@ -135,41 +141,9 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[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 />
|
||||
public void OnMerge(object merged)
|
||||
{
|
||||
if (ExternalIDs != null)
|
||||
foreach (MetadataID<Show> id in ExternalIDs)
|
||||
id.First = this;
|
||||
if (People != null)
|
||||
foreach (PeopleRole link in People)
|
||||
link.Show = this;
|
||||
|
@ -6,7 +6,7 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// A studio that make shows.
|
||||
/// </summary>
|
||||
public class Studio : IResource
|
||||
public class Studio : IResource, IMetadata
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -24,6 +24,9 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Show> Shows { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty, <see cref="Studio"/>.
|
||||
/// </summary>
|
||||
|
@ -1,12 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A single user of the app.
|
||||
/// </summary>
|
||||
public class User : IResource
|
||||
public class User : IResource, IThumbnails
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
@ -39,6 +38,9 @@ namespace Kyoo.Models
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ExtraData { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<int, string> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of shows the user has finished.
|
||||
/// </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)
|
||||
/// </summary>
|
||||
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>
|
||||
/// Metadata of episode currently watching by an user
|
||||
/// </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>
|
||||
/// Where the player has stopped watching the episode (between 0 and 100).
|
||||
/// </summary>
|
||||
|
@ -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="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
|
||||
/// <returns>The two list merged as an array</returns>
|
||||
public static T[] MergeLists<T>(IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
Func<T, T, bool> isEqual = null)
|
||||
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
|
||||
public static T[] MergeLists<T>([CanBeNull] IEnumerable<T> first,
|
||||
[CanBeNull] IEnumerable<T> second,
|
||||
[CanBeNull] Func<T, T, bool> isEqual = null)
|
||||
{
|
||||
if (first == null)
|
||||
return second.ToArray();
|
||||
return second?.ToArray();
|
||||
if (second == null)
|
||||
return first.ToArray();
|
||||
if (isEqual == null)
|
||||
@ -36,6 +37,98 @@ namespace Kyoo
|
||||
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>
|
||||
/// 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"/>
|
||||
@ -63,16 +156,34 @@ namespace Kyoo
|
||||
}
|
||||
|
||||
/// <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"/>
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <remarks>
|
||||
/// This does the opposite of <see cref="Merge{T}"/>.
|
||||
/// </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>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
/// <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)
|
||||
throw new ArgumentNullException(nameof(first));
|
||||
@ -93,7 +204,26 @@ namespace Kyoo
|
||||
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
|
||||
?? 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);
|
||||
}
|
||||
|
||||
@ -103,17 +233,28 @@ namespace Kyoo
|
||||
}
|
||||
|
||||
/// <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"/>.
|
||||
/// 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"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <example>
|
||||
/// {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>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
[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)
|
||||
return second;
|
||||
@ -125,6 +266,9 @@ namespace Kyoo
|
||||
.Where(x => x.CanRead && x.CanWrite
|
||||
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
||||
|
||||
if (where != null)
|
||||
properties = properties.Where(where);
|
||||
|
||||
foreach (PropertyInfo property in properties)
|
||||
{
|
||||
object oldValue = property.GetValue(first);
|
||||
@ -133,6 +277,23 @@ namespace Kyoo
|
||||
|
||||
if (oldValue?.Equals(defaultValue) != false)
|
||||
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)
|
||||
&& property.PropertyType != typeof(string))
|
||||
{
|
||||
|
@ -325,7 +325,7 @@ namespace Kyoo
|
||||
if (types.Length < 1)
|
||||
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
|
||||
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>
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Exceptions;
|
||||
@ -60,6 +62,7 @@ namespace Kyoo
|
||||
/// All providers of Kyoo. See <see cref="Provider"/>.
|
||||
/// </summary>
|
||||
public DbSet<Provider> Providers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of registered users.
|
||||
/// </summary>
|
||||
@ -84,28 +87,34 @@ namespace Kyoo
|
||||
public DbSet<LibraryItem> LibraryItems { get; set; }
|
||||
|
||||
/// <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>
|
||||
/// <typeparam name="T">The metadata of this type will be returned.</typeparam>
|
||||
/// <returns>A queryable of metadata ids for a type.</returns>
|
||||
public DbSet<MetadataID<T>> MetadataIds<T>()
|
||||
where T : class, IResource
|
||||
public DbSet<MetadataID> MetadataIds<T>()
|
||||
where T : class, IMetadata
|
||||
{
|
||||
return Set<MetadataID<T>>();
|
||||
return Set<MetadataID>(MetadataName<T>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a generic link between two resource types.
|
||||
/// Add a many to many link between two resources.
|
||||
/// </summary>
|
||||
/// <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="T2">The second resource type of the relation. It is the contained resource.</typeparam>
|
||||
/// <returns>All links between the two types.</returns>
|
||||
public DbSet<Link<T1, T2>> Links<T1, T2>()
|
||||
public async Task AddLinks<T1, T2>(int first, int second)
|
||||
where T1 : 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)
|
||||
{ }
|
||||
|
||||
/// <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>
|
||||
/// Set basic configurations (like preventing query tracking)
|
||||
/// </summary>
|
||||
@ -132,6 +167,58 @@ namespace Kyoo
|
||||
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>
|
||||
/// Set database parameters to support every types of Kyoo.
|
||||
/// </summary>
|
||||
@ -140,6 +227,9 @@ namespace Kyoo
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<PeopleRole>()
|
||||
.Ignore(x => x.ForPeople);
|
||||
|
||||
modelBuilder.Entity<Show>()
|
||||
.HasMany(x => x.Seasons)
|
||||
.WithOne(x => x.Show)
|
||||
@ -162,117 +252,26 @@ namespace Kyoo
|
||||
.WithMany(x => x.Shows)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<Provider>()
|
||||
.HasMany(x => x.Libraries)
|
||||
.WithMany(x => x.Providers)
|
||||
.UsingEntity<Link<Library, Provider>>(
|
||||
y => y
|
||||
.HasOne(x => x.First)
|
||||
.WithMany(x => x.ProviderLinks),
|
||||
y => y
|
||||
.HasOne(x => x.Second)
|
||||
.WithMany(x => x.LibraryLinks),
|
||||
y => y.HasKey(Link<Library, 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));
|
||||
_HasManyToMany<Library, Provider>(modelBuilder, x => x.Providers, x => x.Libraries);
|
||||
_HasManyToMany<Library, Collection>(modelBuilder, x => x.Collections, x => x.Libraries);
|
||||
_HasManyToMany<Library, Show>(modelBuilder, x => x.Shows, x => x.Libraries);
|
||||
_HasManyToMany<Collection, Show>(modelBuilder, x => x.Shows, x => x.Collections);
|
||||
_HasManyToMany<Show, Genre>(modelBuilder, x => x.Genres, x => x.Shows);
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.HasMany(x => x.Watched)
|
||||
.WithMany("users")
|
||||
.UsingEntity<Link<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));
|
||||
.WithMany("Users")
|
||||
.UsingEntity(x => x.ToTable(LinkName<User, Show>()));
|
||||
|
||||
modelBuilder.Entity<MetadataID<Show>>()
|
||||
.HasKey(MetadataID<Show>.PrimaryKey);
|
||||
modelBuilder.Entity<MetadataID<Show>>()
|
||||
.HasOne(x => x.First)
|
||||
.WithMany(x => x.ExternalIDs)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
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);
|
||||
_HasMetadata<Collection>(modelBuilder);
|
||||
_HasMetadata<Show>(modelBuilder);
|
||||
_HasMetadata<Season>(modelBuilder);
|
||||
_HasMetadata<Episode>(modelBuilder);
|
||||
_HasMetadata<People>(modelBuilder);
|
||||
_HasMetadata<Studio>(modelBuilder);
|
||||
|
||||
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<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>
|
||||
/// Check if the exception is a duplicated exception.
|
||||
/// </summary>
|
||||
@ -517,14 +533,12 @@ namespace Kyoo
|
||||
/// </summary>
|
||||
private void DiscardChanges()
|
||||
{
|
||||
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged
|
||||
&& x.State != EntityState.Detached))
|
||||
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Detached))
|
||||
{
|
||||
entry.State = EntityState.Detached;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Perform a case insensitive like operation.
|
||||
/// </summary>
|
||||
|
@ -234,16 +234,23 @@ namespace Kyoo.Controllers
|
||||
finally
|
||||
{
|
||||
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
|
||||
Database.ChangeTracker.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An overridable method to edit relation of a resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">The non edited resource</param>
|
||||
/// <param name="changed">The new version of <see cref="resource"/>. This item will be saved on the databse and replace <see cref="resource"/></param>
|
||||
/// <param name="resetOld">A boolean to indicate if all values of resource should be discarded or not.</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="resource">
|
||||
/// The non edited resource
|
||||
/// </param>
|
||||
/// <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)
|
||||
{
|
||||
return Validate(resource);
|
||||
@ -254,7 +261,9 @@ namespace Kyoo.Controllers
|
||||
/// It is also called on the default implementation of <see cref="EditRelations"/>
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute<ComputedAttribute>() != null)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
@ -68,7 +68,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
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 =>
|
||||
{
|
||||
@ -83,8 +83,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true),
|
||||
logo = table.Column<string>(type: "text", nullable: true),
|
||||
logo_extension = table.Column<string>(type: "text", nullable: true)
|
||||
images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -116,7 +115,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
email = table.Column<string>(type: "text", nullable: true),
|
||||
password = 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 =>
|
||||
{
|
||||
@ -127,71 +127,97 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "link_library_collection",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||
collection_id = table.Column<int>(type: "integer", nullable: false),
|
||||
library_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
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(
|
||||
name: "fk_link_library_collection_collections_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_link_library_collection_collections_collection_id",
|
||||
column: x => x.collection_id,
|
||||
principalTable: "collections",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_link_library_collection_libraries_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_link_library_collection_libraries_library_id",
|
||||
column: x => x.library_id,
|
||||
principalTable: "libraries",
|
||||
principalColumn: "id",
|
||||
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(
|
||||
name: "link_library_provider",
|
||||
columns: table => new
|
||||
{
|
||||
first_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),
|
||||
provider_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
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(
|
||||
name: "fk_link_library_provider_libraries_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_link_library_provider_libraries_library_id",
|
||||
column: x => x.library_id,
|
||||
principalTable: "libraries",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_link_library_provider_providers_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_link_library_provider_providers_provider_id",
|
||||
column: x => x.provider_id,
|
||||
principalTable: "providers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "metadata_id_people",
|
||||
name: "people_metadata_id",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
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_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(
|
||||
name: "fk_metadata_id_people_people_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_people_metadata_id_people_people_id",
|
||||
column: x => x.resource_id,
|
||||
principalTable: "people",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_metadata_id_people_providers_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_people_metadata_id_providers_provider_id",
|
||||
column: x => x.provider_id,
|
||||
principalTable: "providers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -209,12 +235,9 @@ namespace Kyoo.Postgresql.Migrations
|
||||
path = table.Column<string>(type: "text", nullable: true),
|
||||
overview = table.Column<string>(type: "text", nullable: true),
|
||||
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),
|
||||
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
poster = table.Column<string>(type: "text", nullable: true),
|
||||
logo = table.Column<string>(type: "text", nullable: true),
|
||||
backdrop = table.Column<string>(type: "text", nullable: true),
|
||||
images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true),
|
||||
is_movie = table.Column<bool>(type: "boolean", nullable: false),
|
||||
studio_id = table.Column<int>(type: "integer", nullable: true)
|
||||
},
|
||||
@ -230,24 +253,50 @@ namespace Kyoo.Postgresql.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "link_collection_show",
|
||||
name: "studio_metadata_id",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||
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_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(
|
||||
name: "fk_link_collection_show_collections_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_studio_metadata_id_providers_provider_id",
|
||||
column: x => x.provider_id,
|
||||
principalTable: "providers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_studio_metadata_id_studios_studio_id",
|
||||
column: x => x.resource_id,
|
||||
principalTable: "studios",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.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",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_link_collection_show_shows_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_link_collection_show_shows_show_id",
|
||||
column: x => x.show_id,
|
||||
principalTable: "shows",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -257,21 +306,21 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "link_library_show",
|
||||
columns: table => new
|
||||
{
|
||||
first_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),
|
||||
show_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
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(
|
||||
name: "fk_link_library_show_libraries_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_link_library_show_libraries_library_id",
|
||||
column: x => x.library_id,
|
||||
principalTable: "libraries",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_link_library_show_shows_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_link_library_show_shows_show_id",
|
||||
column: x => x.show_id,
|
||||
principalTable: "shows",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -281,21 +330,21 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "link_show_genre",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||
genre_id = table.Column<int>(type: "integer", nullable: false),
|
||||
show_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
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(
|
||||
name: "fk_link_show_genre_genres_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_link_show_genre_genres_genre_id",
|
||||
column: x => x.genre_id,
|
||||
principalTable: "genres",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_link_show_genre_shows_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_link_show_genre_shows_show_id",
|
||||
column: x => x.show_id,
|
||||
principalTable: "shows",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -305,59 +354,32 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "link_user_show",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||
users_id = table.Column<int>(type: "integer", nullable: false),
|
||||
watched_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
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(
|
||||
name: "fk_link_user_show_shows_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_link_user_show_shows_watched_id",
|
||||
column: x => x.watched_id,
|
||||
principalTable: "shows",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_link_user_show_users_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_link_user_show_users_users_id",
|
||||
column: x => x.users_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
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(
|
||||
name: "people_roles",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
for_people = table.Column<bool>(type: "boolean", nullable: false),
|
||||
people_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),
|
||||
@ -393,7 +415,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
overview = table.Column<string>(type: "text", 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),
|
||||
poster = table.Column<string>(type: "text", nullable: true)
|
||||
images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -406,6 +428,32 @@ namespace Kyoo.Postgresql.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "show_metadata_id",
|
||||
columns: table => new
|
||||
{
|
||||
resource_id = table.Column<int>(type: "integer", nullable: false),
|
||||
provider_id = table.Column<int>(type: "integer", nullable: false),
|
||||
data_id = table.Column<string>(type: "text", nullable: true),
|
||||
link = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_show_metadata_id", x => new { x.resource_id, x.provider_id });
|
||||
table.ForeignKey(
|
||||
name: "fk_show_metadata_id_providers_provider_id",
|
||||
column: x => x.provider_id,
|
||||
principalTable: "providers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_show_metadata_id_shows_show_id",
|
||||
column: x => x.resource_id,
|
||||
principalTable: "shows",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "episodes",
|
||||
columns: table => new
|
||||
@ -419,7 +467,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
episode_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),
|
||||
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),
|
||||
overview = table.Column<string>(type: "text", nullable: true),
|
||||
release_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
@ -442,52 +490,52 @@ namespace Kyoo.Postgresql.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "metadata_id_season",
|
||||
name: "season_metadata_id",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
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_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(
|
||||
name: "fk_metadata_id_season_providers_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_season_metadata_id_providers_provider_id",
|
||||
column: x => x.provider_id,
|
||||
principalTable: "providers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_metadata_id_season_seasons_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_season_metadata_id_seasons_season_id",
|
||||
column: x => x.resource_id,
|
||||
principalTable: "seasons",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "metadata_id_episode",
|
||||
name: "episode_metadata_id",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
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_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(
|
||||
name: "fk_metadata_id_episode_episodes_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_episode_metadata_id_episodes_episode_id",
|
||||
column: x => x.resource_id,
|
||||
principalTable: "episodes",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_metadata_id_episode_providers_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_episode_metadata_id_providers_provider_id",
|
||||
column: x => x.provider_id,
|
||||
principalTable: "providers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -526,33 +574,43 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "watched_episodes",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
user_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)
|
||||
},
|
||||
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(
|
||||
name: "fk_watched_episodes_episodes_second_id",
|
||||
column: x => x.second_id,
|
||||
name: "fk_watched_episodes_episodes_episode_id",
|
||||
column: x => x.episode_id,
|
||||
principalTable: "episodes",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_watched_episodes_users_first_id",
|
||||
column: x => x.first_id,
|
||||
name: "fk_watched_episodes_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_collection_metadata_id_provider_id",
|
||||
table: "collection_metadata_id",
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_collections_slug",
|
||||
table: "collections",
|
||||
column: "slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_episode_metadata_id_provider_id",
|
||||
table: "episode_metadata_id",
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_episodes_season_id",
|
||||
table: "episodes",
|
||||
@ -583,54 +641,34 @@ namespace Kyoo.Postgresql.Migrations
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_link_collection_show_second_id",
|
||||
name: "ix_link_collection_show_show_id",
|
||||
table: "link_collection_show",
|
||||
column: "second_id");
|
||||
column: "show_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_link_library_collection_second_id",
|
||||
name: "ix_link_library_collection_library_id",
|
||||
table: "link_library_collection",
|
||||
column: "second_id");
|
||||
column: "library_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_link_library_provider_second_id",
|
||||
name: "ix_link_library_provider_provider_id",
|
||||
table: "link_library_provider",
|
||||
column: "second_id");
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_link_library_show_second_id",
|
||||
name: "ix_link_library_show_show_id",
|
||||
table: "link_library_show",
|
||||
column: "second_id");
|
||||
column: "show_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_link_show_genre_second_id",
|
||||
name: "ix_link_show_genre_show_id",
|
||||
table: "link_show_genre",
|
||||
column: "second_id");
|
||||
column: "show_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_link_user_show_second_id",
|
||||
name: "ix_link_user_show_watched_id",
|
||||
table: "link_user_show",
|
||||
column: "second_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");
|
||||
column: "watched_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_people_slug",
|
||||
@ -638,6 +676,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
column: "slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_people_metadata_id_provider_id",
|
||||
table: "people_metadata_id",
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_people_roles_people_id",
|
||||
table: "people_roles",
|
||||
@ -654,6 +697,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
column: "slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_season_metadata_id_provider_id",
|
||||
table: "season_metadata_id",
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_seasons_show_id_season_number",
|
||||
table: "seasons",
|
||||
@ -666,6 +714,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
column: "slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_show_metadata_id_provider_id",
|
||||
table: "show_metadata_id",
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_shows_slug",
|
||||
table: "shows",
|
||||
@ -677,6 +730,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
table: "shows",
|
||||
column: "studio_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_studio_metadata_id_provider_id",
|
||||
table: "studio_metadata_id",
|
||||
column: "provider_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_studios_slug",
|
||||
table: "studios",
|
||||
@ -702,13 +760,19 @@ namespace Kyoo.Postgresql.Migrations
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_watched_episodes_second_id",
|
||||
name: "ix_watched_episodes_episode_id",
|
||||
table: "watched_episodes",
|
||||
column: "second_id");
|
||||
column: "episode_id");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "collection_metadata_id");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "episode_metadata_id");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "link_collection_show");
|
||||
|
||||
@ -728,20 +792,20 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "link_user_show");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "metadata_id_episode");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "metadata_id_people");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "metadata_id_season");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "metadata_id_show");
|
||||
name: "people_metadata_id");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "people_roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "season_metadata_id");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "show_metadata_id");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "studio_metadata_id");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tracks");
|
||||
|
||||
@ -758,10 +822,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "genres");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "providers");
|
||||
name: "people");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "people");
|
||||
name: "providers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "episodes");
|
File diff suppressed because it is too large
Load Diff
@ -141,7 +141,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
// language=PostgreSQL
|
||||
migrationBuilder.Sql(@"
|
||||
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
|
||||
ELSE 'show'::item_type
|
||||
END AS type
|
||||
@ -149,11 +149,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
WHERE NOT (EXISTS (
|
||||
SELECT 1
|
||||
FROM link_collection_show AS l
|
||||
INNER JOIN collections AS c ON l.first_id = c.id
|
||||
WHERE s.id = l.second_id))
|
||||
INNER JOIN collections AS c ON l.collection_id = c.id
|
||||
WHERE s.id = l.show_id))
|
||||
UNION ALL
|
||||
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");
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using EFCore.NamingConventions.Internal;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
@ -99,9 +101,55 @@ namespace Kyoo.Postgresql
|
||||
.Property(x => x.ExtraData)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
modelBuilder.Entity<LibraryItem>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Collection>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Show>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Season>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Episode>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<People>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Provider>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.Images)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
/// <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 />
|
||||
protected override bool IsDuplicateException(Exception ex)
|
||||
{
|
||||
|
@ -66,7 +66,7 @@ namespace Kyoo.Postgresql
|
||||
x.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
|
||||
if (_environment.IsDevelopment())
|
||||
x.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||
});
|
||||
}, ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
@ -60,7 +60,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
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 =>
|
||||
{
|
||||
@ -75,8 +75,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Logo = table.Column<string>(type: "TEXT", nullable: true),
|
||||
LogoExtension = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Images = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -108,7 +107,8 @@ namespace Kyoo.SqLite.Migrations
|
||||
Email = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Password = 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 =>
|
||||
{
|
||||
@ -116,74 +116,100 @@ namespace Kyoo.SqLite.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Link<Library, Collection>",
|
||||
name: "LinkLibraryCollection",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
CollectionID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LibraryID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
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(
|
||||
name: "FK_Link<Library, Collection>_Collections_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_LinkLibraryCollection_Collections_CollectionID",
|
||||
column: x => x.CollectionID,
|
||||
principalTable: "Collections",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Link<Library, Collection>_Libraries_FirstID",
|
||||
column: x => x.FirstID,
|
||||
name: "FK_LinkLibraryCollection_Libraries_LibraryID",
|
||||
column: x => x.LibraryID,
|
||||
principalTable: "Libraries",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Link<Library, Provider>",
|
||||
name: "CollectionMetadataID",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
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_Link<Library, Provider>", x => new { x.FirstID, x.SecondID });
|
||||
table.PrimaryKey("PK_CollectionMetadataID", x => new { x.ResourceID, x.ProviderID });
|
||||
table.ForeignKey(
|
||||
name: "FK_Link<Library, Provider>_Libraries_FirstID",
|
||||
column: x => x.FirstID,
|
||||
principalTable: "Libraries",
|
||||
name: "FK_CollectionMetadataID_Collections_ResourceID",
|
||||
column: x => x.ResourceID,
|
||||
principalTable: "Collections",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Link<Library, Provider>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_CollectionMetadataID_Providers_ProviderID",
|
||||
column: x => x.ProviderID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataID<People>",
|
||||
name: "LinkLibraryProvider",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LibraryID = 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),
|
||||
Link = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
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(
|
||||
name: "FK_MetadataID<People>_People_FirstID",
|
||||
column: x => x.FirstID,
|
||||
name: "FK_PeopleMetadataID_People_ResourceID",
|
||||
column: x => x.ResourceID,
|
||||
principalTable: "People",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<People>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_PeopleMetadataID_Providers_ProviderID",
|
||||
column: x => x.ProviderID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -201,12 +227,9 @@ namespace Kyoo.SqLite.Migrations
|
||||
Path = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TrailerUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
EndAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Poster = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Logo = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Backdrop = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Images = table.Column<string>(type: "TEXT", nullable: true),
|
||||
IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
StudioID = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
@ -222,134 +245,133 @@ namespace Kyoo.SqLite.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Link<Collection, Show>",
|
||||
name: "StudioMetadataID",
|
||||
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<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),
|
||||
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_MetadataID<Show>", x => new { x.FirstID, x.SecondID });
|
||||
table.PrimaryKey("PK_StudioMetadataID", x => new { x.ResourceID, x.ProviderID });
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Show>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_StudioMetadataID_Providers_ProviderID",
|
||||
column: x => x.ProviderID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Show>_Shows_FirstID",
|
||||
column: x => x.FirstID,
|
||||
name: "FK_StudioMetadataID_Studios_ResourceID",
|
||||
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",
|
||||
principalColumn: "ID",
|
||||
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(
|
||||
name: "PeopleRoles",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ForPeople = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
PeopleID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ShowID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", nullable: true),
|
||||
@ -385,7 +407,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartDate = 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 =>
|
||||
{
|
||||
@ -398,6 +420,32 @@ namespace Kyoo.SqLite.Migrations
|
||||
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(
|
||||
name: "Episodes",
|
||||
columns: table => new
|
||||
@ -411,7 +459,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
EpisodeNumber = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
AbsoluteNumber = table.Column<int>(type: "INTEGER", 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),
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ReleaseDate = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
@ -434,52 +482,52 @@ namespace Kyoo.SqLite.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataID<Season>",
|
||||
name: "SeasonMetadataID",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
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_MetadataID<Season>", x => new { x.FirstID, x.SecondID });
|
||||
table.PrimaryKey("PK_SeasonMetadataID", x => new { x.ResourceID, x.ProviderID });
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Season>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_SeasonMetadataID_Providers_ProviderID",
|
||||
column: x => x.ProviderID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Season>_Seasons_FirstID",
|
||||
column: x => x.FirstID,
|
||||
name: "FK_SeasonMetadataID_Seasons_ResourceID",
|
||||
column: x => x.ResourceID,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataID<Episode>",
|
||||
name: "EpisodeMetadataID",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
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_MetadataID<Episode>", x => new { x.FirstID, x.SecondID });
|
||||
table.PrimaryKey("PK_EpisodeMetadataID", x => new { x.ResourceID, x.ProviderID });
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Episode>_Episodes_FirstID",
|
||||
column: x => x.FirstID,
|
||||
name: "FK_EpisodeMetadataID_Episodes_ResourceID",
|
||||
column: x => x.ResourceID,
|
||||
principalTable: "Episodes",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Episode>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_EpisodeMetadataID_Providers_ProviderID",
|
||||
column: x => x.ProviderID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@ -518,33 +566,43 @@ namespace Kyoo.SqLite.Migrations
|
||||
name: "WatchedEpisodes",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
UserID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
EpisodeID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
WatchedPercentage = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID });
|
||||
table.PrimaryKey("PK_WatchedEpisodes", x => new { x.UserID, x.EpisodeID });
|
||||
table.ForeignKey(
|
||||
name: "FK_WatchedEpisodes_Episodes_SecondID",
|
||||
column: x => x.SecondID,
|
||||
name: "FK_WatchedEpisodes_Episodes_EpisodeID",
|
||||
column: x => x.EpisodeID,
|
||||
principalTable: "Episodes",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_WatchedEpisodes_Users_FirstID",
|
||||
column: x => x.FirstID,
|
||||
name: "FK_WatchedEpisodes_Users_UserID",
|
||||
column: x => x.UserID,
|
||||
principalTable: "Users",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CollectionMetadataID_ProviderID",
|
||||
table: "CollectionMetadataID",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Collections_Slug",
|
||||
table: "Collections",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EpisodeMetadataID_ProviderID",
|
||||
table: "EpisodeMetadataID",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Episodes_SeasonID",
|
||||
table: "Episodes",
|
||||
@ -575,54 +633,34 @@ namespace Kyoo.SqLite.Migrations
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link<Collection, Show>_SecondID",
|
||||
table: "Link<Collection, Show>",
|
||||
column: "SecondID");
|
||||
name: "IX_LinkCollectionShow_ShowID",
|
||||
table: "LinkCollectionShow",
|
||||
column: "ShowID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link<Library, Collection>_SecondID",
|
||||
table: "Link<Library, Collection>",
|
||||
column: "SecondID");
|
||||
name: "IX_LinkLibraryCollection_LibraryID",
|
||||
table: "LinkLibraryCollection",
|
||||
column: "LibraryID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link<Library, Provider>_SecondID",
|
||||
table: "Link<Library, Provider>",
|
||||
column: "SecondID");
|
||||
name: "IX_LinkLibraryProvider_ProviderID",
|
||||
table: "LinkLibraryProvider",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link<Library, Show>_SecondID",
|
||||
table: "Link<Library, Show>",
|
||||
column: "SecondID");
|
||||
name: "IX_LinkLibraryShow_ShowID",
|
||||
table: "LinkLibraryShow",
|
||||
column: "ShowID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link<Show, Genre>_SecondID",
|
||||
table: "Link<Show, Genre>",
|
||||
column: "SecondID");
|
||||
name: "IX_LinkShowGenre_ShowID",
|
||||
table: "LinkShowGenre",
|
||||
column: "ShowID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link<User, Show>_SecondID",
|
||||
table: "Link<User, Show>",
|
||||
column: "SecondID");
|
||||
|
||||
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");
|
||||
name: "IX_LinkUserShow_WatchedID",
|
||||
table: "LinkUserShow",
|
||||
column: "WatchedID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_People_Slug",
|
||||
@ -630,6 +668,11 @@ namespace Kyoo.SqLite.Migrations
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PeopleMetadataID_ProviderID",
|
||||
table: "PeopleMetadataID",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PeopleRoles_PeopleID",
|
||||
table: "PeopleRoles",
|
||||
@ -646,6 +689,11 @@ namespace Kyoo.SqLite.Migrations
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeasonMetadataID_ProviderID",
|
||||
table: "SeasonMetadataID",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Seasons_ShowID_SeasonNumber",
|
||||
table: "Seasons",
|
||||
@ -658,6 +706,11 @@ namespace Kyoo.SqLite.Migrations
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ShowMetadataID_ProviderID",
|
||||
table: "ShowMetadataID",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shows_Slug",
|
||||
table: "Shows",
|
||||
@ -669,6 +722,11 @@ namespace Kyoo.SqLite.Migrations
|
||||
table: "Shows",
|
||||
column: "StudioID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StudioMetadataID_ProviderID",
|
||||
table: "StudioMetadataID",
|
||||
column: "ProviderID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Studios_Slug",
|
||||
table: "Studios",
|
||||
@ -694,46 +752,52 @@ namespace Kyoo.SqLite.Migrations
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WatchedEpisodes_SecondID",
|
||||
name: "IX_WatchedEpisodes_EpisodeID",
|
||||
table: "WatchedEpisodes",
|
||||
column: "SecondID");
|
||||
column: "EpisodeID");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<Collection, Show>");
|
||||
name: "CollectionMetadataID");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<Library, Collection>");
|
||||
name: "EpisodeMetadataID");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<Library, Provider>");
|
||||
name: "LinkCollectionShow");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<Library, Show>");
|
||||
name: "LinkLibraryCollection");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<Show, Genre>");
|
||||
name: "LinkLibraryProvider");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<User, Show>");
|
||||
name: "LinkLibraryShow");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<Episode>");
|
||||
name: "LinkShowGenre");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<People>");
|
||||
name: "LinkUserShow");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<Season>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<Show>");
|
||||
name: "PeopleMetadataID");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PeopleRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeasonMetadataID");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ShowMetadataID");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "StudioMetadataID");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tracks");
|
||||
|
||||
@ -750,10 +814,10 @@ namespace Kyoo.SqLite.Migrations
|
||||
name: "Genres");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Providers");
|
||||
name: "People");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "People");
|
||||
name: "Providers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Episodes");
|
File diff suppressed because it is too large
Load Diff
@ -154,19 +154,19 @@ namespace Kyoo.SqLite.Migrations
|
||||
// language=SQLite
|
||||
migrationBuilder.Sql(@"
|
||||
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
|
||||
ELSE 0
|
||||
END AS Type
|
||||
FROM Shows AS s
|
||||
WHERE NOT (EXISTS (
|
||||
SELECT 1
|
||||
FROM 'Link<Collection, Show>' AS l
|
||||
INNER JOIN Collections AS c ON l.FirstID = c.ID
|
||||
WHERE s.ID = l.SecondID))
|
||||
FROM LinkCollectionShow AS l
|
||||
INNER JOIN Collections AS c ON l.CollectionID = c.ID
|
||||
WHERE s.ID = l.ShowID))
|
||||
UNION ALL
|
||||
SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 3 AS Status,
|
||||
NULL AS StartAir, NULL AS EndAir, c0.Poster, 2 AS Type
|
||||
SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 0 AS Status,
|
||||
NULL AS StartAir, NULL AS EndAir, c0.Images, 2 AS Type
|
||||
FROM collections AS c0");
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -102,12 +102,41 @@ namespace Kyoo.SqLite
|
||||
.Property(x => x.Type)
|
||||
.HasConversion<int>();
|
||||
|
||||
ValueConverter<Dictionary<string, string>, string> jsonConvertor = new(
|
||||
ValueConverter<Dictionary<string, string>, string> extraDataConvertor = new(
|
||||
x => JsonConvert.SerializeObject(x),
|
||||
x => JsonConvert.DeserializeObject<Dictionary<string, string>>(x));
|
||||
modelBuilder.Entity<User>()
|
||||
.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);
|
||||
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>()
|
||||
.ToView("LibraryItems")
|
||||
@ -115,6 +144,24 @@ namespace Kyoo.SqLite
|
||||
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 />
|
||||
protected override bool IsDuplicateException(Exception ex)
|
||||
{
|
||||
|
@ -66,7 +66,7 @@ namespace Kyoo.SqLite
|
||||
x.UseSqlite(_configuration.GetDatabaseConnection("sqlite"));
|
||||
if (_environment.IsDevelopment())
|
||||
x.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||
});
|
||||
}, ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
79
Kyoo.TheMovieDb/Convertors/CollectionConvertors.cs
Normal file
79
Kyoo.TheMovieDb/Convertors/CollectionConvertors.cs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
47
Kyoo.TheMovieDb/Convertors/EpisodeConvertors.cs
Normal file
47
Kyoo.TheMovieDb/Convertors/EpisodeConvertors.cs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
101
Kyoo.TheMovieDb/Convertors/MovieConvertors.cs
Normal file
101
Kyoo.TheMovieDb/Convertors/MovieConvertors.cs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
182
Kyoo.TheMovieDb/Convertors/PeopleConvertors.cs
Normal file
182
Kyoo.TheMovieDb/Convertors/PeopleConvertors.cs
Normal 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}"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
45
Kyoo.TheMovieDb/Convertors/SeasonConvertors.cs
Normal file
45
Kyoo.TheMovieDb/Convertors/SeasonConvertors.cs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
98
Kyoo.TheMovieDb/Convertors/ShowConvertors.cs
Normal file
98
Kyoo.TheMovieDb/Convertors/ShowConvertors.cs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
60
Kyoo.TheMovieDb/Convertors/StudioConvertors.cs
Normal file
60
Kyoo.TheMovieDb/Convertors/StudioConvertors.cs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
35
Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj
Normal file
35
Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj
Normal 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>
|
79
Kyoo.TheMovieDb/PluginTmdb.cs
Normal file
79
Kyoo.TheMovieDb/PluginTmdb.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
280
Kyoo.TheMovieDb/ProviderTmdb.cs
Normal file
280
Kyoo.TheMovieDb/ProviderTmdb.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
18
Kyoo.TheMovieDb/TheMovieDbOptions.cs
Normal file
18
Kyoo.TheMovieDb/TheMovieDbOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Kyoo.Models;
|
||||
@ -47,7 +48,7 @@ namespace Kyoo.TheTvdb
|
||||
/// <returns>A show representing the given search result.</returns>
|
||||
public static Show ToShow(this SeriesSearchResult result, Provider provider)
|
||||
{
|
||||
return new()
|
||||
return new Show
|
||||
{
|
||||
Slug = result.Slug,
|
||||
Title = result.SeriesName,
|
||||
@ -55,14 +56,19 @@ namespace Kyoo.TheTvdb
|
||||
Overview = result.Overview,
|
||||
Status = _GetStatus(result.Status),
|
||||
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[]
|
||||
{
|
||||
new MetadataID<Show>
|
||||
new MetadataID
|
||||
{
|
||||
DataID = result.Id.ToString(),
|
||||
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>
|
||||
public static Show ToShow(this Series series, Provider provider)
|
||||
{
|
||||
return new()
|
||||
return new Show
|
||||
{
|
||||
Slug = series.Slug,
|
||||
Title = series.SeriesName,
|
||||
@ -84,16 +90,23 @@ namespace Kyoo.TheTvdb
|
||||
Overview = series.Overview,
|
||||
Status = _GetStatus(series.Status),
|
||||
StartAir = _ParseDate(series.FirstAired),
|
||||
Poster = series.Poster != null ? $"https://www.thetvdb.com/banners/{series.Poster}" : null,
|
||||
Backdrop = series.FanArt != null ? $"https://www.thetvdb.com/banners/{series.FanArt}" : null,
|
||||
Images = new Dictionary<int, string>
|
||||
{
|
||||
[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(),
|
||||
ExternalIDs = new[]
|
||||
{
|
||||
new MetadataID<Show>
|
||||
new MetadataID
|
||||
{
|
||||
DataID = series.Id.ToString(),
|
||||
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"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public static PeopleRole ToPeopleRole(this Actor actor, Provider provider)
|
||||
public static PeopleRole ToPeopleRole(this Actor actor)
|
||||
{
|
||||
return new()
|
||||
return new PeopleRole
|
||||
{
|
||||
People = new People
|
||||
{
|
||||
Slug = Utility.ToSlug(actor.Name),
|
||||
Name = actor.Name,
|
||||
Poster = actor.Image != null ? $"https://www.thetvdb.com/banners/{actor.Image}" : null,
|
||||
ExternalIDs = new []
|
||||
Images = new Dictionary<int, string>
|
||||
{
|
||||
new MetadataID<People>()
|
||||
{
|
||||
DataID = actor.Id.ToString(),
|
||||
Link = $"https://www.thetvdb.com/people/{actor.Id}",
|
||||
Second = provider
|
||||
}
|
||||
[Images.Poster] = !string.IsNullOrEmpty(actor.Image)
|
||||
? $"https://www.thetvdb.com/banners/{actor.Image}"
|
||||
: null
|
||||
}
|
||||
},
|
||||
Role = actor.Role,
|
||||
@ -137,21 +145,26 @@ namespace Kyoo.TheTvdb
|
||||
/// <returns>A episode representing the given tvdb episode.</returns>
|
||||
public static Episode ToEpisode(this EpisodeRecord episode, Provider provider)
|
||||
{
|
||||
return new()
|
||||
return new Episode
|
||||
{
|
||||
SeasonNumber = episode.AiredSeason,
|
||||
EpisodeNumber = episode.AiredEpisodeNumber,
|
||||
AbsoluteNumber = episode.AbsoluteNumber,
|
||||
Title = episode.EpisodeName,
|
||||
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[]
|
||||
{
|
||||
new MetadataID<Episode>
|
||||
new MetadataID
|
||||
{
|
||||
DataID = episode.Id.ToString(),
|
||||
Link = $"https://www.thetvdb.com/series/{episode.SeriesId}/episodes/{episode.Id}",
|
||||
Second = provider
|
||||
Provider = provider
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -32,8 +32,10 @@ namespace Kyoo.TheTvdb
|
||||
{
|
||||
Slug = "the-tvdb",
|
||||
Name = "TheTVDB",
|
||||
LogoExtension = "png",
|
||||
Logo = "https://www.thetvdb.com/images/logo.png"
|
||||
Images = new Dictionary<int, string>
|
||||
{
|
||||
[Images.Logo] = "https://www.thetvdb.com/images/logo.png"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -93,7 +95,7 @@ namespace Kyoo.TheTvdb
|
||||
Show ret = series.Data.ToShow(Provider);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit c037270d3339fcf0075984a089f353c5c332a751
|
||||
Subproject commit dca10903ff54a8999732695b5c2a0a5c94f85200
|
18
Kyoo.sln
18
Kyoo.sln
@ -5,8 +5,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Common", "Kyoo.Common\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.CommonAPI", "Kyoo.CommonAPI\Kyoo.CommonAPI.csproj", "{6F91B645-F785-46BB-9C4F-1EFC83E489B6}"
|
||||
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}"
|
||||
EndProject
|
||||
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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheTvdb", "Kyoo.TheTvdb\Kyoo.TheTvdb.csproj", "{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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.Build.0 = Debug|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}.Release|Any CPU.ActiveCfg = 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
|
||||
EndGlobal
|
||||
|
@ -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>
|
@ -8,7 +8,9 @@ using Autofac.Features.Metadata;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
@ -23,14 +25,31 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
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>
|
||||
/// Create a new <see cref="FileSystemComposite"/> from a list of <see cref="IFileSystem"/> mapped to their
|
||||
/// metadata.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
_libraryManager = libraryManager;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
|
||||
@ -88,6 +107,15 @@ namespace Kyoo.Controllers
|
||||
.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 />
|
||||
public Task<Stream> NewFile(string path)
|
||||
{
|
||||
@ -132,12 +160,41 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetExtraDirectory(Show show)
|
||||
public async Task<string> GetExtraDirectory<T>(T resource)
|
||||
{
|
||||
if (show == null)
|
||||
throw new ArgumentNullException(nameof(show));
|
||||
return _GetFileSystemForPath(show.Path, out string _)
|
||||
.GetExtraDirectory(show);
|
||||
switch (resource)
|
||||
{
|
||||
case Season season:
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
@ -45,6 +45,16 @@ namespace Kyoo.Controllers
|
||||
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 />
|
||||
public Task<Stream> NewFile(string path)
|
||||
{
|
||||
@ -76,7 +86,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <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.");
|
||||
}
|
||||
@ -85,6 +95,8 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// An <see cref="IActionResult"/> to proxy an http request.
|
||||
/// </summary>
|
||||
// TODO remove this suppress message once the class has been implemented.
|
||||
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
|
||||
public class HttpForwardResult : IActionResult
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -4,8 +4,10 @@ using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
@ -20,6 +22,20 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
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>
|
||||
/// Get the content type of a file using it's extension.
|
||||
/// </summary>
|
||||
@ -63,6 +79,16 @@ namespace Kyoo.Controllers
|
||||
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 />
|
||||
public Task<Stream> NewFile(string path)
|
||||
{
|
||||
@ -104,11 +130,18 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetExtraDirectory(Show show)
|
||||
public Task<string> GetExtraDirectory<T>(T resource)
|
||||
{
|
||||
string path = Path.Combine(show.Path, "Extra");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
if (!_options.CurrentValue.MetadataInShow)
|
||||
return Task.FromResult<string>(null);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,11 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <summary>
|
||||
/// A provider repository to handle externalID creation and deletion
|
||||
/// </summary>
|
||||
private readonly IProviderRepository _providers;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Expression<Func<Collection, object>> DefaultSort => x => x.Name;
|
||||
|
||||
@ -25,17 +30,19 @@ namespace Kyoo.Controllers
|
||||
/// Create a new <see cref="CollectionRepository"/>.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
_database = database;
|
||||
_providers = providers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Collection>> Search(string query)
|
||||
{
|
||||
return await _database.Collections
|
||||
.Where(_database.Like<Collection>(x => x.Name, $"%{query}%"))
|
||||
.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
|
||||
.OrderBy(DefaultSort)
|
||||
.Take(20)
|
||||
.ToListAsync();
|
||||
@ -50,6 +57,40 @@ namespace Kyoo.Controllers
|
||||
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 />
|
||||
public override async Task Delete(Collection obj)
|
||||
{
|
||||
|
@ -99,7 +99,7 @@ namespace Kyoo.Controllers
|
||||
public override async Task<ICollection<Episode>> Search(string query)
|
||||
{
|
||||
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}%"))
|
||||
.OrderBy(DefaultSort)
|
||||
.Take(20)
|
||||
@ -111,7 +111,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_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).");
|
||||
return await ValidateTracks(obj);
|
||||
}
|
||||
@ -119,8 +118,7 @@ namespace Kyoo.Controllers
|
||||
/// <inheritdoc />
|
||||
protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
|
||||
{
|
||||
if (resource.ShowID <= 0)
|
||||
throw new InvalidOperationException($"Can't store an episode not related to any show (showID: {resource.ShowID}).");
|
||||
await Validate(changed);
|
||||
|
||||
if (changed.Tracks != null || resetOld)
|
||||
{
|
||||
@ -134,8 +132,6 @@ namespace Kyoo.Controllers
|
||||
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
|
||||
resource.ExternalIDs = changed.ExternalIDs;
|
||||
}
|
||||
|
||||
await Validate(resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -145,12 +141,16 @@ namespace Kyoo.Controllers
|
||||
/// <returns>The <see cref="resource"/> parameter is returned.</returns>
|
||||
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.EpisodeSlug = resource.Slug;
|
||||
return _tracks.Create(x);
|
||||
}).ToListAsync());
|
||||
}).ToListAsync();
|
||||
_database.Tracks.AttachRange(resource.Tracks);
|
||||
return resource;
|
||||
}
|
||||
|
||||
@ -158,12 +158,24 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(Episode resource)
|
||||
{
|
||||
await base.Validate(resource);
|
||||
await resource.ExternalIDs.ForEachAsync(async x =>
|
||||
if (resource.ShowID <= 0)
|
||||
{
|
||||
x.Second = await _providers.CreateIfNotExists(x.Second);
|
||||
x.SecondID = x.Second.ID;
|
||||
_database.Entry(x.Second).State = EntityState.Detached;
|
||||
});
|
||||
if (resource.Show == null)
|
||||
throw new ArgumentException($"Can't store an episode not related " +
|
||||
$"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 />
|
||||
|
@ -43,7 +43,7 @@ namespace Kyoo.Controllers
|
||||
public override async Task<ICollection<Library>> Search(string query)
|
||||
{
|
||||
return await _database.Libraries
|
||||
.Where(_database.Like<Library>(x => x.Name, $"%{query}%"))
|
||||
.Where(_database.Like<Library>(x => x.Name + " " + x.Slug, $"%{query}%"))
|
||||
.OrderBy(DefaultSort)
|
||||
.Take(20)
|
||||
.ToListAsync();
|
||||
@ -54,7 +54,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_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).");
|
||||
return obj;
|
||||
}
|
||||
@ -63,20 +62,7 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(Library 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))
|
||||
throw new ArgumentException("The library's slug must be set and not empty");
|
||||
if (string.IsNullOrEmpty(resource.Name))
|
||||
@ -84,9 +70,22 @@ namespace Kyoo.Controllers
|
||||
if (resource.Paths == null || !resource.Paths.Any())
|
||||
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);
|
||||
|
||||
if (changed.Providers != null || resetOld)
|
||||
{
|
||||
await Database.Entry(resource).Collection(x => x.Providers).LoadAsync();
|
||||
resource.Providers = changed.Providers;
|
||||
}
|
||||
|
@ -62,7 +62,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_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).");
|
||||
return obj;
|
||||
}
|
||||
@ -71,23 +70,35 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(People resource)
|
||||
{
|
||||
await base.Validate(resource);
|
||||
await resource.ExternalIDs.ForEachAsync(async id =>
|
||||
|
||||
if (resource.ExternalIDs != null)
|
||||
{
|
||||
id.Second = await _providers.CreateIfNotExists(id.Second);
|
||||
id.SecondID = id.Second.ID;
|
||||
_database.Entry(id.Second).State = EntityState.Detached;
|
||||
});
|
||||
await resource.Roles.ForEachAsync(async role =>
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
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;
|
||||
_database.Entry(role.Show).State = EntityState.Detached;
|
||||
});
|
||||
_database.Entry(role).State = EntityState.Added;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task EditRelations(People resource, People changed, bool resetOld)
|
||||
{
|
||||
await Validate(changed);
|
||||
|
||||
if (changed.Roles != null || resetOld)
|
||||
{
|
||||
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();
|
||||
resource.ExternalIDs = changed.ExternalIDs;
|
||||
|
||||
}
|
||||
await base.EditRelations(resource, changed, resetOld);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -18,12 +18,9 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Expression<Func<Provider, object>> DefaultSort => x => x.Slug;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ProviderRepository"/>.
|
||||
/// Create a new <see cref="ProviderRepository" />.
|
||||
/// </summary>
|
||||
/// <param name="database">The database handle</param>
|
||||
public ProviderRepository(DatabaseContext database)
|
||||
@ -32,6 +29,9 @@ namespace Kyoo.Controllers
|
||||
_database = database;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Expression<Func<Provider, object>> DefaultSort => x => x.Slug;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Provider>> Search(string query)
|
||||
{
|
||||
@ -47,7 +47,8 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_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;
|
||||
}
|
||||
|
||||
@ -62,14 +63,15 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
|
||||
Sort<MetadataID<T>> sort = default,
|
||||
public Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
|
||||
Sort<MetadataID> sort = default,
|
||||
Pagination limit = default)
|
||||
where T : class, IResource
|
||||
where T : class, IMetadata
|
||||
{
|
||||
return ApplyFilters(_database.MetadataIds<T>().Include(y => y.Second),
|
||||
x => _database.MetadataIds<T>().FirstOrDefaultAsync(y => y.FirstID == x),
|
||||
x => x.FirstID,
|
||||
return ApplyFilters(_database.MetadataIds<T>()
|
||||
.Include(y => y.Provider),
|
||||
x => _database.MetadataIds<T>().FirstOrDefaultAsync(y => y.ResourceID == x),
|
||||
x => x.ResourceID,
|
||||
where,
|
||||
sort,
|
||||
limit);
|
||||
|
@ -87,7 +87,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_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).");
|
||||
return obj;
|
||||
}
|
||||
@ -95,32 +94,37 @@ namespace Kyoo.Controllers
|
||||
/// <inheritdoc/>
|
||||
protected override async Task Validate(Season resource)
|
||||
{
|
||||
await base.Validate(resource);
|
||||
if (resource.ShowID <= 0)
|
||||
{
|
||||
if (resource.Show == null)
|
||||
throw new InvalidOperationException(
|
||||
throw new ArgumentException(
|
||||
$"Can't store a season not related to any show (showID: {resource.ShowID}).");
|
||||
resource.ShowID = resource.Show.ID;
|
||||
}
|
||||
|
||||
await base.Validate(resource);
|
||||
await resource.ExternalIDs.ForEachAsync(async id =>
|
||||
if (resource.ExternalIDs != null)
|
||||
{
|
||||
id.Second = await _providers.CreateIfNotExists(id.Second);
|
||||
id.SecondID = id.Second.ID;
|
||||
_database.Entry(id.Second).State = EntityState.Detached;
|
||||
});
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
}
|
||||
_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
|
||||
{
|
||||
await Validate(changed);
|
||||
|
||||
if (changed.ExternalIDs != null || resetOld)
|
||||
{
|
||||
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
|
||||
resource.ExternalIDs = changed.ExternalIDs;
|
||||
}
|
||||
await base.EditRelations(resource, changed, resetOld);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -76,9 +76,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_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).");
|
||||
return obj;
|
||||
}
|
||||
@ -88,29 +85,40 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Validate(resource);
|
||||
if (resource.Studio != null)
|
||||
{
|
||||
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
|
||||
resource.StudioID = resource.Studio.ID;
|
||||
}
|
||||
|
||||
resource.GenreLinks = resource.Genres?
|
||||
.Select(x => Link.Create(resource, x))
|
||||
.ToList();
|
||||
await resource.GenreLinks.ForEachAsync(async id =>
|
||||
if (resource.Genres != null)
|
||||
{
|
||||
id.Second = await _genres.CreateIfNotExists(id.Second);
|
||||
id.SecondID = id.Second.ID;
|
||||
_database.Entry(id.Second).State = EntityState.Detached;
|
||||
});
|
||||
await resource.ExternalIDs.ForEachAsync(async id =>
|
||||
resource.Genres = await resource.Genres
|
||||
.SelectAsync(x => _genres.CreateIfNotExists(x))
|
||||
.ToListAsync();
|
||||
_database.AttachRange(resource.Genres);
|
||||
}
|
||||
|
||||
if (resource.ExternalIDs != null)
|
||||
{
|
||||
id.Second = await _providers.CreateIfNotExists(id.Second);
|
||||
id.SecondID = id.Second.ID;
|
||||
_database.Entry(id.Second).State = EntityState.Detached;
|
||||
});
|
||||
await resource.People.ForEachAsync(async role =>
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
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;
|
||||
_database.Entry(role.People).State = EntityState.Detached;
|
||||
});
|
||||
_database.Entry(role).State = EntityState.Added;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -151,21 +159,18 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
if (collectionID != null)
|
||||
{
|
||||
await _database.Links<Collection, Show>()
|
||||
.AddAsync(new Link<Collection, Show>(collectionID.Value, showID));
|
||||
await _database.AddLinks<Collection, Show>(collectionID.Value, showID);
|
||||
await _database.SaveIfNoDuplicates();
|
||||
|
||||
if (libraryID != null)
|
||||
{
|
||||
await _database.Links<Library, Collection>()
|
||||
.AddAsync(new Link<Library, Collection>(libraryID.Value, collectionID.Value));
|
||||
await _database.AddLinks<Library, Collection>(libraryID.Value, collectionID.Value);
|
||||
await _database.SaveIfNoDuplicates();
|
||||
}
|
||||
}
|
||||
if (libraryID != null)
|
||||
{
|
||||
await _database.Links<Library, Show>()
|
||||
.AddAsync(new Link<Library, Show>(libraryID.Value, showID));
|
||||
await _database.AddLinks<Library, Show>(libraryID.Value, showID);
|
||||
await _database.SaveIfNoDuplicates();
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,11 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
/// <summary>
|
||||
/// A provider repository to handle externalID creation and deletion
|
||||
/// </summary>
|
||||
private readonly IProviderRepository _providers;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Expression<Func<Studio, object>> DefaultSort => x => x.Name;
|
||||
|
||||
@ -26,10 +31,12 @@ namespace Kyoo.Controllers
|
||||
/// Create a new <see cref="StudioRepository"/>.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
_database = database;
|
||||
_providers = providers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -51,6 +58,34 @@ namespace Kyoo.Controllers
|
||||
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 />
|
||||
public override async Task Delete(Studio obj)
|
||||
{
|
||||
|
@ -37,19 +37,25 @@ namespace Kyoo.Controllers
|
||||
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 />
|
||||
public override async Task<Track> Create(Track obj)
|
||||
{
|
||||
if (obj == null)
|
||||
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);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
await _database.SaveChangesAsync();
|
||||
|
@ -1,11 +1,10 @@
|
||||
using Kyoo.Models;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
@ -22,54 +21,17 @@ namespace Kyoo.Controllers
|
||||
/// A logger to report errors.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Create a new <see cref="ThumbnailsManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="files">The file manager to use.</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,
|
||||
ILogger<ThumbnailsManager> logger,
|
||||
IOptionsMonitor<BasicOptions> options,
|
||||
Lazy<ILibraryManager> library)
|
||||
ILogger<ThumbnailsManager> logger)
|
||||
{
|
||||
_files = files;
|
||||
_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>
|
||||
@ -86,8 +48,12 @@ namespace Kyoo.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
await using Stream reader = await _files.GetReader(url);
|
||||
await using Stream local = await _files.NewFile(localPath);
|
||||
AsyncRef<string> mime = new();
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
@ -98,195 +64,74 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download images of a specified show.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
|
||||
where T : IThumbnails
|
||||
{
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
|
||||
if (item.Images == null)
|
||||
return false;
|
||||
|
||||
string name = item is IResource res ? res.Slug : "???";
|
||||
bool ret = false;
|
||||
|
||||
if (show.Poster != null)
|
||||
foreach ((int id, string image) in item.Images.Where(x => x.Value != null))
|
||||
{
|
||||
string posterPath = await GetPoster(show);
|
||||
if (alwaysDownload || !await _files.Exists(posterPath))
|
||||
ret |= await _DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}");
|
||||
}
|
||||
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}");
|
||||
string localPath = await _GetPrivateImagePath(item, id);
|
||||
if (alwaysDownload || !await _files.Exists(localPath))
|
||||
ret |= await _DownloadImage(image, localPath, $"The image n°{id} of {name}");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download images of a specified person.
|
||||
/// Retrieve the local path of an image of the given item <b>without an extension</b>.
|
||||
/// </summary>
|
||||
/// <param name="people">
|
||||
/// 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] 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
|
||||
/// <param name="item">The item to retrieve the poster from.</param>
|
||||
/// <param name="imageID">The ID of the image. See <see cref="Images"/> for values.</param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>The path of the image for the given resource, <b>even if it does not exists</b></returns>
|
||||
private async Task<string> _GetPrivateImagePath<T>(T item, int imageID)
|
||||
{
|
||||
if (item == null)
|
||||
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")),
|
||||
Season season => _GetSeasonPoster(season),
|
||||
People actor => Task.FromResult(_files.Combine(_options.CurrentValue.PeoplePath, $"{actor.Slug}.jpg")),
|
||||
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a poster.")
|
||||
Images.Poster => "poster",
|
||||
Images.Logo => "logo",
|
||||
Images.Thumbnail => "thumbnail",
|
||||
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>
|
||||
/// 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");
|
||||
return _files.Combine(directory, imageName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetThumbnail<T>(T item)
|
||||
where T : IResource
|
||||
public async Task<string> GetImagePath<T>(T item, int imageID)
|
||||
where T : IThumbnails
|
||||
{
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
return item switch
|
||||
{
|
||||
Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "backdrop.jpg")),
|
||||
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.")
|
||||
});
|
||||
string basePath = await _GetPrivateImagePath(item, imageID);
|
||||
string directory = Path.GetDirectoryName(basePath);
|
||||
string baseFile = Path.GetFileName(basePath);
|
||||
return (await _files.ListFiles(directory!))
|
||||
.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == baseFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,10 +88,8 @@ namespace Kyoo.Controllers
|
||||
|
||||
public async Task<Track[]> ExtractInfos(Episode episode, bool reextract)
|
||||
{
|
||||
if (episode.Show == null)
|
||||
await _library.Value.Load(episode, x => x.Show);
|
||||
|
||||
string dir = _files.GetExtraDirectory(episode.Show);
|
||||
string dir = await _files.GetExtraDirectory(episode.Show);
|
||||
if (dir == null)
|
||||
throw new ArgumentException("Invalid path.");
|
||||
return await Task.Factory.StartNew(
|
||||
|
@ -101,13 +101,13 @@ namespace Kyoo
|
||||
/// <inheritdoc />
|
||||
public void Configure(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterComposite<FileSystemComposite, IFileSystem>();
|
||||
builder.RegisterComposite<FileSystemComposite, IFileSystem>().InstancePerLifetimeScope();
|
||||
builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance();
|
||||
builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance();
|
||||
|
||||
builder.RegisterType<ConfigurationManager>().As<IConfigurationManager>().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<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
|
||||
builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance();
|
||||
|
@ -40,7 +40,7 @@
|
||||
<PackageReference Include="Autofac.Extras.AttributeMetadata" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<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="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
@ -25,16 +25,6 @@ namespace Kyoo.Models.Options
|
||||
/// </summary>
|
||||
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>
|
||||
/// The temporary folder to cache transmuxed file.
|
||||
/// </summary>
|
||||
@ -44,5 +34,22 @@ namespace Kyoo.Models.Options
|
||||
/// The temporary folder to cache transcoded file.
|
||||
/// </summary>
|
||||
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/";
|
||||
}
|
||||
}
|
@ -116,16 +116,11 @@ namespace Kyoo.Tasks
|
||||
else
|
||||
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);
|
||||
|
||||
if (season != null)
|
||||
season.Show = show;
|
||||
season = await _RegisterAndFill(season);
|
||||
if (season != null)
|
||||
season.Title ??= $"Season {season.SeasonNumber}";
|
||||
progress.Report(60);
|
||||
|
||||
episode.Show = show;
|
||||
@ -163,16 +158,32 @@ namespace Kyoo.Tasks
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>The existing or filled item.</returns>
|
||||
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))
|
||||
return null;
|
||||
|
||||
T existing = await _libraryManager.GetOrDefault<T>(item.Slug);
|
||||
if (existing != null)
|
||||
{
|
||||
await _libraryManager.Load(existing, x => x.ExternalIDs);
|
||||
return existing;
|
||||
}
|
||||
|
||||
item = await _metadataProvider.Get(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);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.CommonApi;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Kyoo.Models.Options;
|
||||
using Kyoo.Models.Permissions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -19,11 +20,18 @@ namespace Kyoo.Api
|
||||
public class CollectionApi : CrudApi<Collection>
|
||||
{
|
||||
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)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_files = files;
|
||||
_thumbs = thumbs;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/show")]
|
||||
@ -129,5 +137,48 @@ namespace Kyoo.Api
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -195,7 +195,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -210,7 +210,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -94,7 +94,7 @@ namespace Kyoo.Api
|
||||
People people = await _libraryManager.GetOrDefault<People>(id);
|
||||
if (people == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbs.GetPoster(people));
|
||||
return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster));
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/poster")]
|
||||
@ -103,7 +103,7 @@ namespace Kyoo.Api
|
||||
People people = await _libraryManager.GetOrDefault<People>(slug);
|
||||
if (people == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbs.GetPoster(people));
|
||||
return _files.FileResult(await _thumbs.GetImagePath(people, Images.Poster));
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ namespace Kyoo.Api
|
||||
Provider provider = await _libraryManager.GetOrDefault<Provider>(id);
|
||||
if (provider == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbnails.GetLogo(provider));
|
||||
return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo));
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/logo")]
|
||||
@ -45,7 +45,7 @@ namespace Kyoo.Api
|
||||
Provider provider = await _libraryManager.GetOrDefault<Provider>(slug);
|
||||
if (provider == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbnails.GetLogo(provider));
|
||||
return _files.FileResult(await _thumbnails.GetImagePath(provider, Images.Logo));
|
||||
}
|
||||
}
|
||||
}
|
@ -151,7 +151,7 @@ namespace Kyoo.Api
|
||||
if (season == null)
|
||||
return NotFound();
|
||||
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")]
|
||||
@ -161,7 +161,7 @@ namespace Kyoo.Api
|
||||
if (season == null)
|
||||
return NotFound();
|
||||
await _libraryManager.Load(season, x => x.Show);
|
||||
return _files.FileResult(await _thumbs.GetPoster(season));
|
||||
return _files.FileResult(await _thumbs.GetImagePath(season, Images.Poster));
|
||||
}
|
||||
}
|
||||
}
|
@ -383,7 +383,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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))
|
||||
.ToDictionary(Path.GetFileNameWithoutExtension,
|
||||
x => $"{BaseURL}api/shows/{slug}/fonts/{Path.GetFileName(x)}");
|
||||
@ -402,7 +402,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
@ -417,7 +417,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -431,7 +431,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -446,7 +446,7 @@ namespace Kyoo.Api
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -3,10 +3,10 @@
|
||||
"url": "http://*:5000",
|
||||
"publicUrl": "http://localhost:5000/",
|
||||
"pluginsPath": "plugins/",
|
||||
"peoplePath": "people/",
|
||||
"providerPath": "providers/",
|
||||
"transmuxPath": "cached/transmux",
|
||||
"transcodePath": "cached/transcode"
|
||||
"transcodePath": "cached/transcode",
|
||||
"metadataInShow": true,
|
||||
"metadataPath": "metadata/"
|
||||
},
|
||||
|
||||
"database": {
|
||||
@ -70,5 +70,8 @@
|
||||
|
||||
"tvdb": {
|
||||
"apiKey": "REDACTED"
|
||||
},
|
||||
"the-moviedb": {
|
||||
"apiKey": "REDACTED"
|
||||
}
|
||||
}
|
||||
|
76
tests/Kyoo.Tests/Database/RepositoryActivator.cs
Normal file
76
tests/Kyoo.Tests/Database/RepositoryActivator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
198
tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
Normal file
198
tests/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
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;
|
||||
|
||||
@ -59,7 +62,8 @@ namespace Kyoo.Tests.Database
|
||||
episode = await _repository.Edit(new Episode
|
||||
{
|
||||
ID = 1,
|
||||
SeasonNumber = 2
|
||||
SeasonNumber = 2,
|
||||
ShowID = 1
|
||||
}, false);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
|
||||
episode = await _repository.Get(1);
|
||||
@ -74,7 +78,8 @@ namespace Kyoo.Tests.Database
|
||||
episode = await _repository.Edit(new Episode
|
||||
{
|
||||
ID = 1,
|
||||
EpisodeNumber = 2
|
||||
EpisodeNumber = 2,
|
||||
ShowID = 1
|
||||
}, false);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||
episode = await _repository.Get(1);
|
||||
@ -93,10 +98,6 @@ namespace Kyoo.Tests.Database
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug);
|
||||
}
|
||||
|
||||
|
||||
// TODO absolute numbering tests
|
||||
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteSlugTest()
|
||||
{
|
||||
@ -133,7 +134,8 @@ namespace Kyoo.Tests.Database
|
||||
Episode episode = await _repository.Edit(new Episode
|
||||
{
|
||||
ID = 2,
|
||||
AbsoluteNumber = 56
|
||||
AbsoluteNumber = 56,
|
||||
ShowID = 1
|
||||
}, false);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
|
||||
episode = await _repository.Get(2);
|
||||
@ -148,7 +150,8 @@ namespace Kyoo.Tests.Database
|
||||
{
|
||||
ID = 2,
|
||||
SeasonNumber = 1,
|
||||
EpisodeNumber = 2
|
||||
EpisodeNumber = 2,
|
||||
ShowID = 1
|
||||
}, false);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||
episode = await _repository.Get(2);
|
||||
@ -188,5 +191,137 @@ namespace Kyoo.Tests.Database
|
||||
Episode episode = await _repository.Get(3);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
@ -55,7 +55,7 @@ namespace Kyoo.Tests.Database
|
||||
[Fact]
|
||||
public async Task GetCollectionTests()
|
||||
{
|
||||
LibraryItem expected = new(TestSample.Get<Show>());
|
||||
LibraryItem expected = new(TestSample.Get<Collection>());
|
||||
LibraryItem actual = await _repository.Get(-1);
|
||||
KAssert.DeepEqual(expected, actual);
|
||||
}
|
||||
@ -79,9 +79,10 @@ namespace Kyoo.Tests.Database
|
||||
[Fact]
|
||||
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));
|
||||
}
|
158
tests/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
Normal file
158
tests/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
170
tests/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs
Normal file
170
tests/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -16,13 +16,6 @@ namespace Kyoo.Tests.Database
|
||||
_repositories = new RepositoryActivator(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
|
||||
public void SampleTest()
|
||||
{
|
||||
Assert.False(ReferenceEquals(TestSample.Get<Show>(), TestSample.Get<Show>()));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_repositories.Dispose();
|
||||
@ -33,5 +26,12 @@ namespace Kyoo.Tests.Database
|
||||
{
|
||||
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
Loading…
x
Reference in New Issue
Block a user