Delete dotnet file system abstraction

This commit is contained in:
Zoe Roux 2023-07-31 15:01:00 +09:00
parent ae901b257b
commit c69864d0f5
62 changed files with 191 additions and 6712 deletions

View File

@ -29,13 +29,6 @@
</PropertyGroup>
<PropertyGroup>
<IsWindows Condition="$([MSBuild]::IsOSPlatform('Windows'))">true</IsWindows>
<IsOSX Condition="$([MSBuild]::IsOSPlatform('OSX'))">true</IsOSX>
<IsLinux Condition="$([MSBuild]::IsOSPlatform('Linux'))">true</IsLinux>
<!-- This is horrible but the only workarround I found to disable the transcoder copy on the dev dockerfile -->
<SkipTranscoder Condition="$(MsBuildThisFileDirectory) == '/app/src/'">true</SkipTranscoder>
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
</PropertyGroup>

View File

@ -1,139 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// A service to abstract the file system to allow custom file systems
/// (like distant file systems or external providers).
/// </summary>
public interface IFileSystem
{
/// <summary>
/// Used for http queries returning a file. This should be used to return local files
/// or proxy them from a distant server.
/// </summary>
/// <remarks>
/// If no file exists at the given path or if the path is null, a NotFoundResult is returned
/// to handle it gracefully.
/// </remarks>
/// <param name="path">The path of the file.</param>
/// <param name="rangeSupport">
/// Should the file be downloaded at once or is the client allowed to request only part of the file
/// </param>
/// <param name="type">
/// You can manually specify the content type of your file.
/// For example you can force a file to be returned as plain text using <c>text/plain</c>.
/// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file).
/// </param>
/// <returns>An <see cref="IActionResult"/> representing the file returned.</returns>
IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null);
/// <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>
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
/// <returns>A reader to read the file.</returns>
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>
Task<Stream> GetReader([NotNull] string path, AsyncRef<string> mime);
/// <summary>
/// Create a new file at <paramref name="path"></paramref>.
/// </summary>
/// <param name="path">The path of the new file.</param>
/// <returns>A writer to write to the new file.</returns>
Task<Stream> NewFile([NotNull] string path);
/// <summary>
/// Create a new directory at the given path
/// </summary>
/// <param name="path">The path of the directory</param>
/// <returns>The path of the newly created directory is returned.</returns>
Task<string> CreateDirectory([NotNull] string path);
/// <summary>
/// Combine multiple paths.
/// </summary>
/// <param name="paths">The paths to combine</param>
/// <returns>The combined path.</returns>
string Combine(params string[] paths);
/// <summary>
/// List files in a directory.
/// </summary>
/// <param name="path">The path of the directory</param>
/// <param name="options">Should the search be recursive or not.</param>
/// <returns>A list of files's path.</returns>
Task<ICollection<string>> ListFiles([NotNull] string path,
SearchOption options = SearchOption.TopDirectoryOnly);
/// <summary>
/// Check if a file exists at the given path.
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path exists, false otherwise</returns>
Task<bool> Exists([NotNull] string path);
/// <summary>
/// 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="resource">The resource to proceed</param>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <returns>The extra directory of the resource.</returns>
Task<string> GetExtraDirectory<T>([NotNull] T resource);
/// <summary>
/// Retrieve tracks for a specific episode.
/// Subtitles, chapters and fonts should also be extracted and cached when calling this method.
/// </summary>
/// <param name="episode">The episode to retrieve tracks for.</param>
/// <param name="reExtract">Should the cache be invalidated and subtitles and others be re-extracted?</param>
/// <returns>The list of tracks available for this episode.</returns>
Task<ICollection<Track>> ExtractInfos([NotNull] Episode episode, bool reExtract);
/// <summary>
/// Transmux the selected episode to hls.
/// </summary>
/// <param name="episode">The episode to transmux.</param>
/// <returns>The master file (m3u8) of the transmuxed hls file.</returns>
IActionResult Transmux([NotNull] Episode episode);
// Maybe add options for to select the codec.
// IActionResult Transcode(Episode episode);
}
}

View File

@ -70,11 +70,6 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
IEpisodeRepository EpisodeRepository { get; }
/// <summary>
/// The repository that handle tracks.
/// </summary>
ITrackRepository TrackRepository { get; }
/// <summary>
/// The repository that handle people.
/// </summary>

View File

@ -330,11 +330,6 @@ namespace Kyoo.Abstractions.Controllers
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
}
/// <summary>
/// A repository to handle tracks
/// </summary>
public interface ITrackRepository : IRepository<Track> { }
/// <summary>
/// A repository to handle libraries.
/// </summary>

View File

@ -17,9 +17,10 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
#nullable enable
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
@ -39,17 +40,17 @@ namespace Kyoo.Abstractions.Controllers
/// </param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
Task<bool> DownloadImages<T>([NotNull] T item, bool alwaysDownload = false)
Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
where T : IThumbnails;
/// <summary>
/// Retrieve the local path of an image of the given item.
/// </summary>
/// <param name="item">The item to retrieve the poster from.</param>
/// <param name="imageID">The ID of the image. See <see cref="Images"/> for values.</param>
/// <param name="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 or null if it does not exists.</returns>
Task<string> GetImagePath<T>([NotNull] T item, int imageID)
string? GetImagePath<T>(T item, int imageId)
where T : IThumbnails;
}
}

View File

@ -1,63 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// Transcoder responsible of handling low level video details.
/// </summary>
public interface ITranscoder
{
/// <summary>
/// Retrieve tracks for a specific episode.
/// Subtitles, chapters and fonts should also be extracted and cached when calling this method.
/// </summary>
/// <param name="episode">The episode to retrieve tracks for.</param>
/// <param name="reExtract">Should the cache be invalidated and subtitles and others be re-extracted?</param>
/// <returns>The list of tracks available for this episode.</returns>
Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract);
/// <summary>
/// List fonts assosiated with this episode.
/// </summary>
/// <param name="episode">Th episode to list fonts for.</param>
/// <returns>The list of attachements for this epiosode.</returns>
Task<ICollection<Font>> ListFonts(Episode episode);
/// <summary>
/// Get the specified font for this episode.
/// </summary>
/// <param name="episode">The episode to list fonts for.</param>
/// <param name="slug">The slug of the specific font to retrive.</param>
/// <returns>The <see cref="Font"/> with the given slug or null.</returns>
[ItemCanBeNull] Task<Font> GetFont(Episode episode, string slug);
/// <summary>
/// Transmux the selected episode to hls.
/// </summary>
/// <param name="episode">The episode to transmux.</param>
/// <returns>The master file (m3u8) of the transmuxed hls file.</returns>
IActionResult Transmux(Episode episode);
}
}

View File

@ -1,70 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Kyoo.Abstractions.Controllers;
namespace Kyoo.Abstractions.Models.Attributes
{
/// <summary>
/// An attribute to inform how a <see cref="IFileSystem"/> works.
/// </summary>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
public class FileSystemMetadataAttribute : Attribute
{
/// <summary>
/// The scheme(s) used to identify this path.
/// It can be something like http, https, ftp, file and so on.
/// </summary>
/// <remarks>
/// If multiples files with the same schemes exists, an exception will be thrown.
/// </remarks>
public string[] Scheme { get; }
/// <summary>
/// <c>true</c> if the scheme should be removed from the path before calling
/// methods of this <see cref="IFileSystem"/>, <c>false</c> otherwise.
/// </summary>
public bool StripScheme { get; set; }
/// <summary>
/// Create a new <see cref="FileSystemMetadataAttribute"/> using the specified schemes.
/// </summary>
/// <param name="schemes">The schemes to use.</param>
public FileSystemMetadataAttribute(string[] schemes)
{
Scheme = schemes;
}
/// <summary>
/// Create a new <see cref="FileSystemMetadataAttribute"/> using a dictionary of metadata.
/// </summary>
/// <param name="metadata">
/// The dictionary of metadata. This method expect the dictionary to contain a field
/// per property in this attribute, with the same types as the properties of this attribute.
/// </param>
public FileSystemMetadataAttribute(IDictionary<string, object> metadata)
{
Scheme = (string[])metadata[nameof(Scheme)];
StripScheme = (bool)metadata[nameof(StripScheme)];
}
}
}

View File

@ -1,94 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Kyoo.Abstractions.Controllers;
namespace Kyoo.Abstractions.Models.Attributes
{
/// <summary>
/// An attribute to inform how a <see cref="IFileSystem"/> works.
/// </summary>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
public class TaskMetadataAttribute : Attribute
{
/// <summary>
/// The slug of the task, used to start it.
/// </summary>
public string Slug { get; }
/// <summary>
/// The name of the task that will be displayed to the user.
/// </summary>
public string Name { get; }
/// <summary>
/// A quick description of what this task will do.
/// </summary>
public string Description { get; }
/// <summary>
/// Should this task be automatically run at app startup?
/// </summary>
public bool RunOnStartup { get; set; }
/// <summary>
/// The priority of this task. Only used if <see cref="RunOnStartup"/> is true.
/// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
/// </summary>
public int Priority { get; set; }
/// <summary>
/// <c>true</c> if this task should not be displayed to the user, <c>false</c> otherwise.
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Create a new <see cref="TaskMetadataAttribute"/> with the given slug, name and description.
/// </summary>
/// <param name="slug">The slug of the task, used to start it.</param>
/// <param name="name">The name of the task that will be displayed to the user.</param>
/// <param name="description">A quick description of what this task will do.</param>
public TaskMetadataAttribute(string slug, string name, string description)
{
Slug = slug;
Name = name;
Description = description;
}
/// <summary>
/// Create a new <see cref="TaskMetadataAttribute"/> using a dictionary of metadata.
/// </summary>
/// <param name="metadata">
/// The dictionary of metadata. This method expect the dictionary to contain a field
/// per property in this attribute, with the same types as the properties of this attribute.
/// </param>
public TaskMetadataAttribute(IDictionary<string, object> metadata)
{
Slug = (string)metadata[nameof(Slug)];
Name = (string)metadata[nameof(Name)];
Description = (string)metadata[nameof(Description)];
RunOnStartup = (bool)metadata[nameof(RunOnStartup)];
Priority = (int)metadata[nameof(Priority)];
IsHidden = (bool)metadata[nameof(IsHidden)];
}
}
}

View File

@ -1,56 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// A chapter to split an episode in multiple parts.
/// </summary>
public class Chapter
{
/// <summary>
/// The start time of the chapter (in second from the start of the episode).
/// </summary>
public float StartTime { get; set; }
/// <summary>
/// The end time of the chapter (in second from the start of the episode).
/// </summary>
public float EndTime { get; set; }
/// <summary>
/// The name of this chapter. This should be a human-readable name that could be presented to the user.
/// There should be well-known chapters name for commonly used chapters.
/// For example, use "Opening" for the introduction-song and "Credits" for the end chapter with credits.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Create a new <see cref="Chapter"/>.
/// </summary>
/// <param name="startTime">The start time of the chapter (in second)</param>
/// <param name="endTime">The end time of the chapter (in second)</param>
/// <param name="name">The name of this chapter</param>
public Chapter(float startTime, float endTime, string name)
{
StartTime = startTime;
EndTime = endTime;
Name = name;
}
}
}

View File

@ -1,47 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Runtime.Serialization;
namespace Kyoo.Abstractions.Models.Exceptions
{
/// <summary>
/// An exception thrown when a part of the app has a fatal issue.
/// </summary>
[Serializable]
public class HealthException : Exception
{
/// <summary>
/// Create a new <see cref="HealthException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use.</param>
public HealthException(string message)
: base(message)
{ }
/// <summary>
/// The serialization constructor
/// </summary>
/// <param name="info">Serialization infos</param>
/// <param name="context">The serialization context</param>
protected HealthException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
}

View File

@ -1,72 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using PathIO = System.IO.Path;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// A font of an <see cref="Episode"/>.
/// </summary>
public class Font : ILink
{
/// <summary>
/// A human-readable identifier, used in the URL.
/// </summary>
public string Slug { get; set; }
/// <summary>
/// The name of the font file (with the extension).
/// </summary>
public string File { get; set; }
/// <summary>
/// The format of this font (the extension).
/// </summary>
public string Format { get; set; }
/// <summary>
/// The path of the font.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <inheritdoc/>
public object Link { get; init; }
/// <summary>
/// Create a new empty <see cref="Font"/>.
/// </summary>
public Font() { }
/// <summary>
/// Create a new <see cref="Font"/> from a path.
/// </summary>
/// <param name="path">The path of the font.</param>
/// <param name="episodeSlug">The slug of the episode that contains this font.</param>
public Font(string path, string episodeSlug)
{
Slug = Utility.ToSlug(PathIO.GetFileNameWithoutExtension(path));
Path = path;
File = PathIO.GetFileName(path);
Format = PathIO.GetExtension(path).Replace(".", string.Empty);
Link = $"/watch/{episodeSlug}/font/{Slug}.{Format}";
}
}
}

View File

@ -123,7 +123,7 @@ namespace Kyoo.Abstractions.Models
public int? AbsoluteNumber { get; set; }
/// <summary>
/// The path of the video file for this episode. Any format supported by a <see cref="IFileSystem"/> is allowed.
/// The path of the video file for this episode.
/// </summary>
public string Path { get; set; }
@ -148,11 +148,6 @@ namespace Kyoo.Abstractions.Models
/// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// <summary>
/// The list of tracks this episode has. This lists video, audio and subtitles available.
/// </summary>
[EditableRelation][LoadableRelation] public ICollection<Track> Tracks { get; set; }
/// <summary>
/// Get the slug of an episode.
/// </summary>

View File

@ -28,7 +28,6 @@ namespace Kyoo.Abstractions.Models
{
/// <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"/>

View File

@ -46,7 +46,6 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The path of the root directory of this show.
/// This can be any kind of path supported by <see cref="IFileSystem"/>
/// </summary>
[SerializeIgnore] public string Path { get; set; }
@ -61,7 +60,7 @@ namespace Kyoo.Abstractions.Models
public Status Status { get; set; }
/// <summary>
/// An URL to a trailer. This could be any path supported by the <see cref="IFileSystem"/>.
/// An URL to a trailer.
/// </summary>
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
[Obsolete("Use Images instead of this, this is only kept for the API response.")]

View File

@ -1,229 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// The list of available stream types.
/// Attachments are only used temporarily by the transcoder but are not stored in a database.
/// </summary>
public enum StreamType
{
/// <summary>
/// The type of the stream is not known.
/// </summary>
Unknown = 0,
/// <summary>
/// The stream is a video.
/// </summary>
Video = 1,
/// <summary>
/// The stream is an audio.
/// </summary>
Audio = 2,
/// <summary>
/// The stream is a subtitle.
/// </summary>
Subtitle = 3,
}
/// <summary>
/// A video, audio or subtitle track for an episode.
/// </summary>
public class Track : IResource, ILink
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
[Computed]
public string Slug
{
get
{
string type = Type.ToString().ToLowerInvariant();
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString(CultureInfo.InvariantCulture);
return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : string.Empty)}.{type}";
}
[UsedImplicitly]
private set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
Match match = Regex.Match(value,
@"(?<ep>[^\.]+)\.(?<lang>\w{0,3})(-(?<index>\d+))?(\.(?<forced>forced))?\.(?<type>\w+)(\.\w*)?");
if (!match.Success)
{
throw new ArgumentException(
"Invalid track slug. " +
"Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]"
);
}
EpisodeSlug = match.Groups["ep"].Value;
Language = match.Groups["lang"].Value;
if (Language == "und")
Language = null;
TrackIndex = match.Groups["index"].Success ? int.Parse(match.Groups["index"].Value, CultureInfo.InvariantCulture) : 0;
IsForced = match.Groups["forced"].Success;
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
}
}
/// <summary>
/// The title of the stream.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The language of this stream (as a ISO-639-2 language code)
/// </summary>
public string Language { get; set; }
/// <summary>
/// The codec of this stream.
/// </summary>
public string Codec { get; set; }
/// <summary>
/// Is this stream the default one of it's type?
/// </summary>
public bool IsDefault { get; set; }
/// <summary>
/// Is this stream tagged as forced?
/// </summary>
public bool IsForced { get; set; }
/// <summary>
/// Is this track extern to the episode's file?
/// </summary>
public bool IsExternal { get; set; }
/// <summary>
/// The path of this track.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The type of this stream.
/// </summary>
[SerializeIgnore] public StreamType Type { get; set; }
/// <summary>
/// The ID of the episode that uses this track.
/// </summary>
[SerializeIgnore] public int EpisodeID { get; set; }
/// <summary>
/// The episode that uses this track.
/// </summary>
[LoadableRelation(nameof(EpisodeID))]
public Episode Episode
{
get => _episode;
set
{
_episode = value;
if (_episode != null)
EpisodeSlug = _episode.Slug;
}
}
/// <summary>
/// The index of this track on the episode.
/// </summary>
public int TrackIndex { get; set; }
/// <summary>
/// A user-friendly name for this track. It does not include the track type.
/// </summary>
public string DisplayName
{
get
{
string language = _GetLanguage(Language);
if (language == null)
return $"Unknown (index: {TrackIndex})";
CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures)
.FirstOrDefault(x => x.ThreeLetterISOLanguageName == language);
string name = info?.EnglishName ?? language;
if (IsForced)
name += " Forced";
if (IsExternal)
name += " (External)";
if (Title is { Length: > 1 })
name += " - " + Title;
return name;
}
}
/// <summary>
/// The slug of the episode that contain this track. If this is not set, this track is ill-formed.
/// </summary>
[SerializeIgnore] public string EpisodeSlug { private get; set; }
/// <summary>
/// The episode that uses this track.
/// This is the baking field of <see cref="Episode"/>.
/// </summary>
[SerializeIgnore] private Episode _episode;
/// <inheritdoc/>
public object Link => Type == StreamType.Subtitle ? $"/subtitle/{Slug}" : null;
// Converting mkv track language to c# system language tag.
private static string _GetLanguage(string mkvLanguage)
{
// TODO delete this and have a real way to get the language string from the ISO-639-2.
return mkvLanguage switch
{
"fre" => "fra",
null => "und",
_ => mkvLanguage
};
}
/// <summary>
/// Utility method to create a track slug from a incomplete slug (only add the type of the track).
/// </summary>
/// <param name="baseSlug">The slug to edit</param>
/// <param name="type">The new type of this </param>
/// <returns>The completed slug.</returns>
public static string BuildSlug(string baseSlug, StreamType type)
{
return baseSlug.EndsWith($".{type}", StringComparison.InvariantCultureIgnoreCase)
? baseSlug
: $"{baseSlug}.{type.ToString().ToLowerInvariant()}";
}
}
}

View File

@ -1,35 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
namespace Kyoo.Abstractions.Models
{
/// <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; }
}
}

View File

@ -19,12 +19,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using PathIO = System.IO.Path;
namespace Kyoo.Abstractions.Models
{
@ -85,11 +85,6 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public DateTime? ReleaseDate { get; set; }
/// <summary>
/// The path of the video file for this episode. Any format supported by a <see cref="IFileSystem"/> is allowed.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The episode that come before this one if you follow usual watch orders.
/// If this is the first episode or this is a movie, it will be null.
@ -111,35 +106,9 @@ namespace Kyoo.Abstractions.Models
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// The container of the video file of this episode.
/// Common containers are mp4, mkv, avi and so on.
/// The transcoder's info for this item. This include subtitles, fonts, chapters...
/// </summary>
public string Container { get; set; }
/// <summary>
/// The video track. See <see cref="Track"/> for more information.
/// </summary>
public Track Video { get; set; }
/// <summary>
/// The list of audio tracks. See <see cref="Track"/> for more information.
/// </summary>
public ICollection<Track> Audios { get; set; }
/// <summary>
/// The list of subtitles tracks. See <see cref="Track"/> for more information.
/// </summary>
public ICollection<Track> Subtitles { get; set; }
/// <summary>
/// The list of fonts that can be used to draw the subtitles.
/// </summary>
public ICollection<Font> Fonts { get; set; }
/// <summary>
/// The list of chapters. See <see cref="Chapter"/> for more information.
/// </summary>
public ICollection<Chapter> Chapters { get; set; }
public object Info { get; set; }
[SerializeIgnore]
private string _Type => IsMovie ? "movie" : "episode";
@ -158,13 +127,11 @@ namespace Kyoo.Abstractions.Models
/// <param name="library">
/// A library manager to retrieve the next and previous episode and load the show and tracks of the episode.
/// </param>
/// <param name="fs">A file system used to retrieve chapters informations.</param>
/// <param name="transcoder">The transcoder used to list fonts.</param>
/// <param name="client">A http client to reach the transcoder.</param>
/// <returns>A new WatchItem representing the given episode.</returns>
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library, IFileSystem fs, ITranscoder transcoder)
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library, HttpClient client)
{
await library.Load(ep, x => x.Show);
await library.Load(ep, x => x.Tracks);
return new WatchItem
{
@ -178,13 +145,7 @@ namespace Kyoo.Abstractions.Models
Title = ep.Title,
Overview = ep.Overview,
ReleaseDate = ep.ReleaseDate,
Path = ep.Path,
Images = ep.Show.Images,
Container = PathIO.GetExtension(ep.Path).Replace(".", string.Empty),
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
Fonts = await transcoder.ListFonts(ep),
PreviousEpisode = ep.Show.IsMovie
? null
: (await library.GetAll<Episode>(
@ -197,50 +158,16 @@ namespace Kyoo.Abstractions.Models
where: x => x.ShowID == ep.ShowID,
limit: new Pagination(1, ep.ID)
)).FirstOrDefault(),
Chapters = await _GetChapters(ep, fs),
IsMovie = ep.Show.IsMovie
IsMovie = ep.Show.IsMovie,
Info = await _GetInfo(ep, client),
};
}
// TODO move this method in a controller to support abstraction.
private static async Task<ICollection<Chapter>> _GetChapters(Episode episode, IFileSystem fs)
private static async Task<object> _GetInfo(Episode ep, HttpClient client)
{
string path = fs.Combine(
await fs.GetExtraDirectory(episode),
"Chapters",
PathIO.GetFileNameWithoutExtension(episode.Path) + ".txt"
return await client.GetFromJsonAsync<object>(
$"http://transcoder:7666/info/{(ep.Show.IsMovie ? "movie" : "episode")}/${ep.Slug}/info"
);
if (!await fs.Exists(path))
return Array.Empty<Chapter>();
try
{
using StreamReader sr = new(await fs.GetReader(path));
string chapters = await sr.ReadToEndAsync();
return chapters.Split('\n')
.Select(x =>
{
string[] values = x.Split(' ');
if (
values.Length < 3
|| !float.TryParse(values[0], out float start)
|| !float.TryParse(values[1], out float end)
)
return null;
return new Chapter(
start,
end,
string.Join(' ', values.Skip(2))
);
})
.Where(x => x != null)
.ToArray();
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Invalid chapter file at {path}");
Console.Error.WriteLine(ex.ToString());
return Array.Empty<Chapter>();
}
}
/// <inheritdoc />

View File

@ -1,168 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A <see cref="IFileSystem"/> for http/https links.
/// </summary>
[FileSystemMetadata(new[] { "http", "https" })]
public class HttpFileSystem : IFileSystem
{
/// <summary>
/// The http client factory used to create clients.
/// </summary>
private readonly IHttpClientFactory _clientFactory;
/// <summary>
/// Create a <see cref="HttpFileSystem"/> using the given client factory.
/// </summary>
/// <param name="factory">The http client factory used to create clients.</param>
public HttpFileSystem(IHttpClientFactory factory)
{
_clientFactory = factory;
}
/// <inheritdoc />
public IActionResult FileResult(string path, bool rangeSupport = false, string type = null)
{
if (path == null)
return new NotFoundResult();
return new HttpForwardResult(new Uri(path), rangeSupport, type);
}
/// <inheritdoc />
public Task<Stream> GetReader(string path)
{
HttpClient client = _clientFactory.CreateClient();
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)
{
throw new NotSupportedException("An http filesystem is readonly, a new file can't be created.");
}
/// <inheritdoc />
public Task<string> CreateDirectory(string path)
{
throw new NotSupportedException("An http filesystem is readonly, a directory can't be created.");
}
/// <inheritdoc />
public string Combine(params string[] paths)
{
return Path.Combine(paths);
}
/// <inheritdoc />
public Task<ICollection<string>> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
throw new NotSupportedException("Listing files is not supported on an http filesystem.");
}
/// <inheritdoc />
public Task<bool> Exists(string path)
{
throw new NotSupportedException("Checking if a file exists is not supported on an http filesystem.");
}
/// <inheritdoc />
public Task<string> GetExtraDirectory<T>(T resource)
{
throw new NotSupportedException("Extras can not be stored inside an http filesystem.");
}
/// <inheritdoc />
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
throw new NotSupportedException("Extracting infos is not supported on an http filesystem.");
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
throw new NotSupportedException("Transmuxing is not supported on an http filesystem.");
}
/// <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", Justification = "Not Implemented Yet.")]
public class HttpForwardResult : IActionResult
{
/// <summary>
/// The path of the request to forward.
/// </summary>
private readonly Uri _path;
/// <summary>
/// Should the proxied result support ranges requests?
/// </summary>
private readonly bool _rangeSupport;
/// <summary>
/// If not null, override the content type of the resulting request.
/// </summary>
private readonly string _type;
/// <summary>
/// Create a new <see cref="HttpForwardResult"/>.
/// </summary>
/// <param name="path">The path of the request to forward.</param>
/// <param name="rangeSupport">Should the proxied result support ranges requests?</param>
/// <param name="type">If not null, override the content type of the resulting request.</param>
public HttpForwardResult(Uri path, bool rangeSupport, string type = null)
{
_path = path;
_rangeSupport = rangeSupport;
_type = type;
}
/// <inheritdoc />
public Task ExecuteResultAsync(ActionContext context)
{
// TODO implement that, example: https://github.com/twitchax/AspNetCore.Proxy/blob/14dd0f212d7abb43ca1bf8c890d5efb95db66acb/src/Core/Extensions/Http.cs#L15
throw new NotImplementedException();
}
}
}
}

View File

@ -1,176 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A <see cref="IFileSystem"/> for the local filesystem (using System.IO).
/// </summary>
[FileSystemMetadata(new[] { "", "file" }, StripScheme = true)]
public class LocalFileSystem : IFileSystem
{
/// <summary>
/// An extension provider to get content types from files extensions.
/// </summary>
private readonly IContentTypeProvider _provider;
/// <summary>
/// The transcoder of local files.
/// </summary>
private readonly ITranscoder _transcoder;
/// <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>
/// <param name="provider">An extension provider to get content types from files extensions.</param>
/// <param name="transcoder">The transcoder of local files.</param>
public LocalFileSystem(IOptionsMonitor<BasicOptions> options,
IContentTypeProvider provider,
ITranscoder transcoder)
{
_options = options;
_provider = provider;
_transcoder = transcoder;
}
/// <summary>
/// Get the content type of a file using it's extension.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
/// <returns>The content type of the file</returns>
private string _GetContentType(string path)
{
if (_provider.TryGetContentType(path, out string contentType))
return contentType;
throw new NotImplementedException($"Can't get the content type of the file at: {path}");
}
/// <inheritdoc />
public IActionResult FileResult(string path, bool rangeSupport = false, string type = null)
{
if (path == null)
return new NotFoundResult();
if (!File.Exists(path))
return new NotFoundResult();
return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path))
{
EnableRangeProcessing = rangeSupport
};
}
/// <inheritdoc />
public Task<Stream> GetReader(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return Task.FromResult<Stream>(File.OpenRead(path));
}
/// <inheritdoc />
public Task<Stream> GetReader(string path, AsyncRef<string> mime)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
_provider.TryGetContentType(path, out string mimeValue);
mime.Value = mimeValue;
return Task.FromResult<Stream>(File.OpenRead(path));
}
/// <inheritdoc />
public Task<Stream> NewFile(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return Task.FromResult<Stream>(File.Create(path));
}
/// <inheritdoc />
public Task<string> CreateDirectory(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
Directory.CreateDirectory(path);
return Task.FromResult(path);
}
/// <inheritdoc />
public string Combine(params string[] paths)
{
return Path.Combine(paths);
}
/// <inheritdoc />
public Task<ICollection<string>> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
string[] ret = Directory.Exists(path)
? Directory.GetFiles(path, "*", options)
: Array.Empty<string>();
return Task.FromResult<ICollection<string>>(ret);
}
/// <inheritdoc />
public Task<bool> Exists(string path)
{
return Task.FromResult(File.Exists(path) || Directory.Exists(path));
}
/// <inheritdoc />
public Task<string> GetExtraDirectory<T>(T resource)
{
string path = resource switch
{
IResource res => Combine(_options.CurrentValue.MetadataPath, typeof(T).Name.ToLower(), res.Slug),
_ => Combine(_options.CurrentValue.MetadataPath, typeof(T).Name.ToLower())
};
return CreateDirectory(path);
}
/// <inheritdoc />
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
return _transcoder.ExtractInfos(episode, reExtract);
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
return _transcoder.Transmux(episode);
}
}
}

View File

@ -57,9 +57,6 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc />
public IEpisodeRepository EpisodeRepository { get; }
/// <inheritdoc />
public ITrackRepository TrackRepository { get; }
/// <inheritdoc />
public IPeopleRepository PeopleRepository { get; }
@ -89,7 +86,6 @@ namespace Kyoo.Core.Controllers
ShowRepository = GetRepository<Show>() as IShowRepository;
SeasonRepository = GetRepository<Season>() as ISeasonRepository;
EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
TrackRepository = GetRepository<Track>() as ITrackRepository;
PeopleRepository = GetRepository<People>() as IPeopleRepository;
StudioRepository = GetRepository<Studio>() as IStudioRepository;
GenreRepository = GetRepository<Genre>() as IGenreRepository;
@ -354,11 +350,6 @@ namespace Kyoo.Core.Controllers
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Episode e, nameof(Episode.Tracks)) => _SetRelation(e,
TrackRepository.GetAll(x => x.Episode.ID == obj.ID),
(x, y) => x.Tracks = y,
(x, y) => { x.Episode = y; x.EpisodeID = y.ID; }),
(Episode e, nameof(Episode.Show)) => ShowRepository
.GetOrDefault(x => x.Episodes.Any(y => y.ID == obj.ID))
.Then(x =>
@ -376,15 +367,6 @@ namespace Kyoo.Core.Controllers
}),
(Track t, nameof(Track.Episode)) => EpisodeRepository
.GetOrDefault(x => x.Tracks.Any(y => y.ID == obj.ID))
.Then(x =>
{
t.Episode = x;
t.EpisodeID = x?.ID ?? 0;
}),
(Genre g, nameof(Genre.Shows)) => ShowRepository
.GetAll(x => x.Genres.Any(y => y.ID == obj.ID))
.Then(x => g.Shows = x),

View File

@ -44,11 +44,6 @@ namespace Kyoo.Core.Controllers
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// A track repository to handle creation and deletion of tracks related to the current episode.
/// </summary>
private readonly Lazy<ITrackRepository> _tracks;
/// <inheritdoc />
// Use absolute numbers by default and fallback to season/episodes if it does not exists.
protected override Sort<Episode> DefaultSort => new Sort<Episode>.Conglomerate(
@ -63,16 +58,13 @@ namespace Kyoo.Core.Controllers
/// <param name="database">The database handle to use.</param>
/// <param name="shows">A show repository</param>
/// <param name="providers">A provider repository</param>
/// <param name="tracks">A track repository</param>
public EpisodeRepository(DatabaseContext database,
IShowRepository shows,
IProviderRepository providers,
Lazy<ITrackRepository> tracks)
IProviderRepository providers)
: base(database)
{
_database = database;
_providers = providers;
_tracks = tracks;
// Edit episode slugs when the show's slug changes.
shows.OnEdited += (show) =>
@ -162,7 +154,7 @@ namespace Kyoo.Core.Controllers
? Get(obj.ShowID, obj.SeasonNumber.Value, obj.EpisodeNumber.Value)
: GetAbsolute(obj.ShowID, obj.AbsoluteNumber.Value));
OnResourceCreated(obj);
return await _ValidateTracks(obj);
return obj;
}
/// <inheritdoc />
@ -170,13 +162,6 @@ namespace Kyoo.Core.Controllers
{
await Validate(changed);
if (changed.Tracks != null || resetOld)
{
await _tracks.Value.DeleteAll(x => x.EpisodeID == resource.ID);
resource.Tracks = changed.Tracks;
await _ValidateTracks(resource);
}
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
@ -184,25 +169,6 @@ namespace Kyoo.Core.Controllers
}
}
/// <summary>
/// Set track's index and ensure that every tracks is well-formed.
/// </summary>
/// <param name="resource">The resource to fix.</param>
/// <returns>The <paramref name="resource"/> parameter is returned.</returns>
private async Task<Episode> _ValidateTracks(Episode resource)
{
if (resource.Tracks == null)
return resource;
resource.Tracks = await resource.Tracks.SelectAsync(x =>
{
x.Episode = resource;
return _tracks.Value.Create(x);
}).ToListAsync();
_database.Tracks.AttachRange(resource.Tracks);
return resource;
}
/// <inheritdoc />
protected override async Task Validate(Episode resource)
{
@ -236,7 +202,6 @@ namespace Kyoo.Core.Controllers
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await obj.Tracks.ForEachAsync(x => _tracks.Value.Delete(x));
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync();
await base.Delete(obj);

View File

@ -1,112 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository to handle tracks.
/// </summary>
public class TrackRepository : LocalRepository<Track>, ITrackRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Sort<Track> DefaultSort => new Sort<Track>.By(x => x.TrackIndex);
/// <summary>
/// Create a new <see cref="TrackRepository"/>.
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="episodes">The episode repository</param>
public TrackRepository(DatabaseContext database, IEpisodeRepository episodes)
: base(database)
{
_database = database;
// Edit tracks slugs when the episodes's slug changes.
episodes.OnEdited += (ep) =>
{
List<Track> tracks = _database.Tracks.AsTracking().Where(x => x.EpisodeID == ep.ID).ToList();
foreach (Track track in tracks)
{
track.EpisodeSlug = ep.Slug;
_database.SaveChanges();
OnResourceEdited(track);
}
};
}
/// <inheritdoc />
public override Task<ICollection<Track>> Search(string query)
{
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));
await base.Create(obj);
obj.EpisodeSlug = _database.Episodes.First(x => x.ID == obj.EpisodeID).Slug;
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync();
OnResourceCreated(obj);
return obj;
}
/// <inheritdoc />
public override async Task Delete(Track obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
await base.Delete(obj);
}
}
}

View File

@ -19,13 +19,15 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Kyoo.Core.Controllers
{
/// <summary>
@ -33,30 +35,27 @@ namespace Kyoo.Core.Controllers
/// </summary>
public class ThumbnailsManager : IThumbnailsManager
{
/// <summary>
/// The file manager used to download the image if the file is distant
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// A logger to report errors.
/// </summary>
private readonly ILogger<ThumbnailsManager> _logger;
private readonly IHttpClientFactory _clientFactory;
/// <summary>
/// Create a new <see cref="ThumbnailsManager"/>.
/// </summary>
/// <param name="files">The file manager to use.</param>
/// <param name="clientFactory">Client factory</param>
/// <param name="logger">A logger to report errors</param>
public ThumbnailsManager(IFileSystem files,
public ThumbnailsManager(IHttpClientFactory clientFactory,
ILogger<ThumbnailsManager> logger)
{
_files = files;
_clientFactory = clientFactory;
_logger = logger;
}
/// <summary>
/// An helper function to download an image using a <see cref="LocalFileSystem"/>.
/// An helper function to download an image.
/// </summary>
/// <param name="url">The distant url of the image</param>
/// <param name="localPath">The local path of the image</param>
@ -70,12 +69,17 @@ namespace Kyoo.Core.Controllers
try
{
_logger.LogInformation("Downloading image {What}", what);
AsyncRef<string> mime = new();
await using Stream reader = await _files.GetReader(url, mime);
HttpClient client = _clientFactory.CreateClient();
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string mime = response.Content.Headers.ContentType?.MediaType!;
await using Stream reader = await response.Content.ReadAsStreamAsync();
string extension = new FileExtensionContentTypeProvider()
.Mappings.FirstOrDefault(x => x.Value == mime.Value)
.Mappings.FirstOrDefault(x => x.Value == mime)
.Key;
await using Stream local = await _files.NewFile(localPath + extension);
await using Stream local = File.Create(localPath + extension);
await reader.CopyToAsync(local);
return true;
}
@ -101,8 +105,8 @@ namespace Kyoo.Core.Controllers
foreach ((int id, string image) in item.Images.Where(x => x.Value != null))
{
string localPath = await _GetPrivateImagePath(item, id);
if (alwaysDownload || !await _files.Exists(localPath))
string localPath = _GetPrivateImagePath(item, id);
if (alwaysDownload || !Path.Exists(localPath))
ret |= await _DownloadImage(image, localPath, $"The image n {id} of {name}");
}
@ -113,34 +117,41 @@ namespace Kyoo.Core.Controllers
/// Retrieve the local path of an image of the given item <b>without an extension</b>.
/// </summary>
/// <param name="item">The item to retrieve the poster from.</param>
/// <param name="imageID">The ID of the image. See <see cref="Images"/> for values.</param>
/// <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>([NotNull] T item, int imageID)
private static string _GetPrivateImagePath<T>(T item, int imageId)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
string directory = await _files.GetExtraDirectory(item);
string imageName = imageID switch
string directory = item switch
{
IResource res => Path.Combine("/metadata", typeof(T).Name.ToLowerInvariant(), res.Slug),
_ => Path.Combine("/metadata", typeof(T).Name.ToLowerInvariant())
};
Directory.CreateDirectory(directory);
string imageName = imageId switch
{
Images.Poster => "poster",
Images.Logo => "logo",
Images.Thumbnail => "thumbnail",
Images.Trailer => "trailer",
_ => $"{imageID}"
_ => $"{imageId}"
};
return _files.Combine(directory, imageName);
return Path.Combine(directory, imageName);
}
/// <inheritdoc />
public async Task<string> GetImagePath<T>(T item, int imageID)
public string? GetImagePath<T>(T item, int imageId)
where T : IThumbnails
{
string basePath = await _GetPrivateImagePath(item, imageID);
string directory = Path.GetDirectoryName(basePath);
string basePath = _GetPrivateImagePath(item, imageId);
string directory = Path.GetDirectoryName(basePath)!;
string baseFile = Path.GetFileName(basePath);
return (await _files.ListFiles(directory!))
if (!Directory.Exists(directory))
return null;
return Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly)
.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == baseFile);
}
}

View File

@ -1,342 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Core.Models.Options;
using Kyoo.Core.Models.Watch;
using Kyoo.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// The transcoder used by the <see cref="LocalFileSystem"/>.
/// </summary>
public class Transcoder : ITranscoder
{
#pragma warning disable IDE1006
/// <summary>
/// The class that interact with the transcoder written in C.
/// </summary>
private static class TranscoderAPI
{
/// <summary>
/// The name of the library. For windows '.dll' should be appended, on linux or macos it should be prefixed
/// by 'lib' and '.so' or '.dylib' should be appended.
/// </summary>
private const string TranscoderPath = "transcoder";
/// <summary>
/// Initialize the C library, setup the logger and return the size of a <see cref="FTrack"/>.
/// </summary>
/// <returns>The size of a <see cref="FTrack"/></returns>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern int init();
/// <summary>
/// Initialize the C library, setup the logger and return the size of a <see cref="FTrack"/>.
/// </summary>
/// <returns>The size of a <see cref="FTrack"/></returns>
public static int Init() => init();
/// <summary>
/// Transmux the file at the specified path. The path must be a local one with '/' as a separator.
/// </summary>
/// <param name="path">The path of a local file with '/' as a separators.</param>
/// <param name="outPath">The path of the hls output file.</param>
/// <param name="playableDuration">
/// The number of seconds currently playable. This is incremented as the file gets transmuxed.
/// </param>
/// <returns><c>0</c> on success, non 0 on failure.</returns>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern int transmux(string path, string outPath, out float playableDuration);
/// <summary>
/// Transmux the file at the specified path. The path must be a local one.
/// </summary>
/// <param name="path">The path of a local file.</param>
/// <param name="outPath">The path of the hls output file.</param>
/// <param name="playableDuration">
/// The number of seconds currently playable. This is incremented as the file gets transmuxed.
/// </param>
/// <returns><c>0</c> on success, non 0 on failure.</returns>
public static int Transmux(string path, string outPath, out float playableDuration)
{
path = path.Replace('\\', '/');
outPath = outPath.Replace('\\', '/');
return transmux(path, outPath, out playableDuration);
}
/// <summary>
/// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file.
/// </summary>
/// <param name="path">
/// The path of the video file to analyse. This must be a local path with '/' as a separator.
/// </param>
/// <param name="outPath">The directory that will be used to store extracted files.</param>
/// <param name="length">The size of the returned array.</param>
/// <param name="trackCount">The number of tracks in the returned array.</param>
/// <param name="reExtract">Should the cache be invalidated and information re-extracted or not?</param>
/// <returns>A pointer to an array of <see cref="FTrack"/></returns>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern IntPtr extract_infos(string path,
string outPath,
out uint length,
out uint trackCount,
bool reExtract);
/// <summary>
/// An helper method to free an array of <see cref="FTrack"/>.
/// </summary>
/// <param name="streams">A pointer to the first element of the array</param>
/// <param name="count">The number of items in the array.</param>
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern void free_streams(IntPtr streams, uint count);
/// <summary>
/// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file.
/// </summary>
/// <param name="path">The path of the video file to analyse. This must be a local path.</param>
/// <param name="outPath">The directory that will be used to store extracted files.</param>
/// <param name="reExtract">Should the cache be invalidated and information re-extracted or not?</param>
/// <returns>An array of <see cref="Track"/>.</returns>
public static Track[] ExtractInfos(string path, string outPath, bool reExtract)
{
path = path.Replace('\\', '/');
outPath = outPath.Replace('\\', '/');
int size = Marshal.SizeOf<FTrack>();
IntPtr ptr = extract_infos(path, outPath, out uint arrayLength, out uint trackCount, reExtract);
IntPtr streamsPtr = ptr;
Track[] tracks;
if (trackCount > 0 && ptr != IntPtr.Zero)
{
tracks = new Track[trackCount];
int j = 0;
for (int i = 0; i < arrayLength; i++, streamsPtr += size)
{
FTrack stream = Marshal.PtrToStructure<FTrack>(streamsPtr);
if (stream!.Type == FTrackType.Unknown || stream.Type == FTrackType.Attachment)
continue;
tracks[j] = stream.ToTrack();
j++;
}
Array.Resize(ref tracks, j);
}
else
tracks = Array.Empty<Track>();
if (ptr != IntPtr.Zero)
free_streams(ptr, trackCount);
return tracks;
}
}
#pragma warning restore IDE1006
/// <summary>
/// The file system used to retrieve the extra directory of shows to know where to extract information.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Options to know where to cache transmuxed/transcoded episodes.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// The logger to use. This is also used by the wrapped C library.
/// </summary>
private readonly ILogger<Transcoder> _logger;
/// <summary>
/// <see langword="true"/> if the C library has been checked, <see langword="false"/> otherwise.
/// </summary>
private bool _initialized;
/// <summary>
/// Create a new <see cref="Transcoder"/>.
/// </summary>
/// <param name="files">
/// The file system used to retrieve the extra directory of shows to know where to extract information.
/// </param>
/// <param name="options">Options to know where to cache transmuxed/transcoded episodes.</param>
/// <param name="logger">The logger to use. This is also used by the wrapped C library.</param>
public Transcoder(IFileSystem files, IOptions<BasicOptions> options, ILogger<Transcoder> logger)
{
_files = files;
_options = options;
_logger = logger;
}
/// <summary>
/// Check if the C library can be used or if there is an issue with it.
/// </summary>
/// <param name="fastStop">Should the healthcheck be abborted if the transcoder was already initialized?</param>
/// <exception cref="HealthException">If the transcoder is corrupted, this exception in thrown.</exception>
public void CheckHealth(bool fastStop = false)
{
if (fastStop && _initialized)
return;
if (TranscoderAPI.Init() != Marshal.SizeOf<FTrack>())
{
_logger.LogCritical("The transcoder library could not be initialized correctly");
throw new HealthException("The transcoder library is corrupted or invalid.");
}
_initialized = true;
}
/// <inheritdoc />
public async Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
CheckHealth(true);
string dir = await _files.GetExtraDirectory(episode);
if (dir == null)
throw new ArgumentException("Invalid path.");
return await Task.Factory.StartNew(
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reExtract),
TaskCreationOptions.LongRunning
);
}
/// <inheritdoc/>
public async Task<ICollection<Font>> ListFonts(Episode episode)
{
string path = _files.Combine(await _files.GetExtraDirectory(episode), "Attachments");
return (await _files.ListFiles(path))
.Select(x => new Font(x, episode.Slug))
.ToArray();
}
/// <inheritdoc/>
public async Task<Font> GetFont(Episode episode, string slug)
{
string path = _files.Combine(await _files.GetExtraDirectory(episode), "Attachments");
string font = (await _files.ListFiles(path))
.FirstOrDefault(x => Utility.ToSlug(Path.GetFileNameWithoutExtension(x)) == slug);
if (font == null)
return null;
return new Font(font, episode.Slug);
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
CheckHealth(true);
string folder = Path.Combine(_options.Value.TransmuxPath, episode.Slug);
string manifest = Path.GetFullPath(Path.Combine(folder, episode.Slug + ".m3u8"));
try
{
Directory.CreateDirectory(folder);
if (File.Exists(manifest))
return new PhysicalFileResult(manifest, "application/x-mpegurl");
}
catch (UnauthorizedAccessException)
{
_logger.LogCritical("Access to the path {Manifest} is denied. " +
"Please change your transmux path in the config", manifest);
return new StatusCodeResult(500);
}
return new TransmuxResult(episode.Path, manifest, _logger);
}
/// <summary>
/// An action result that runs the transcoder and return the created manifest file after a few seconds of
/// the video has been proceeded. If the transcoder fails, it returns a 500 error code.
/// </summary>
private class TransmuxResult : IActionResult
{
/// <summary>
/// The path of the episode to transmux. It must be a local one.
/// </summary>
private readonly string _path;
/// <summary>
/// The path of the manifest file to create. It must be a local one.
/// </summary>
private readonly string _manifest;
/// <summary>
/// The logger to use in case of issue.
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// Create a new <see cref="TransmuxResult"/>.
/// </summary>
/// <param name="path">The path of the episode to transmux. It must be a local one.</param>
/// <param name="manifest">The path of the manifest file to create. It must be a local one.</param>
/// <param name="logger">The logger to use in case of issue.</param>
public TransmuxResult(string path, string manifest, ILogger logger)
{
_path = path;
_manifest = Path.GetFullPath(manifest);
_logger = logger;
}
// We use threads so tasks are not always awaited.
#pragma warning disable 4014
/// <inheritdoc />
public async Task ExecuteResultAsync(ActionContext context)
{
float playableDuration = 0;
bool transmuxFailed = false;
Task.Factory.StartNew(() =>
{
transmuxFailed = TranscoderAPI.Transmux(_path, _manifest, out playableDuration) != 0;
}, TaskCreationOptions.LongRunning);
while (playableDuration < 10 || (!File.Exists(_manifest) && !transmuxFailed))
await Task.Delay(10);
if (!transmuxFailed)
{
new PhysicalFileResult(_manifest, "application/x-mpegurl")
.ExecuteResultAsync(context);
}
else
{
_logger.LogCritical("The transmuxing failed on the C library");
new StatusCodeResult(500)
.ExecuteResultAsync(context);
}
}
#pragma warning restore 4014
}
}
}

View File

@ -27,7 +27,6 @@ using Kyoo.Core.Controllers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
@ -46,10 +45,6 @@ namespace Kyoo.Core
/// <inheritdoc />
public void Configure(ContainerBuilder builder)
{
builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance();
builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance();
builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance();
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
@ -59,22 +54,11 @@ namespace Kyoo.Core
builder.RegisterRepository<IShowRepository, ShowRepository>();
builder.RegisterRepository<ISeasonRepository, SeasonRepository>();
builder.RegisterRepository<IEpisodeRepository, EpisodeRepository>();
builder.RegisterRepository<ITrackRepository, TrackRepository>();
builder.RegisterRepository<IPeopleRepository, PeopleRepository>();
builder.RegisterRepository<IStudioRepository, StudioRepository>();
builder.RegisterRepository<IGenreRepository, GenreRepository>();
builder.RegisterRepository<IProviderRepository, ProviderRepository>();
builder.RegisterRepository<IUserRepository, UserRepository>();
builder.RegisterType<FileExtensionContentTypeProvider>().As<IContentTypeProvider>().SingleInstance()
.OnActivating(x =>
{
x.Instance.Mappings[".data"] = "application/octet-stream";
x.Instance.Mappings[".mkv"] = "video/x-matroska";
x.Instance.Mappings[".ass"] = "text/x-ssa";
x.Instance.Mappings[".srt"] = "application/x-subrip";
x.Instance.Mappings[".m3u8"] = "application/x-mpegurl";
});
}
/// <inheritdoc />

View File

@ -2,13 +2,6 @@
<PropertyGroup>
<AssemblyName>Kyoo.Core</AssemblyName>
<RootNamespace>Kyoo.Core</RootNamespace>
<TranscoderRoot>../Kyoo.Transcoder/</TranscoderRoot>
</PropertyGroup>
<PropertyGroup>
<TranscoderBinary Condition="$(IsWindows) == true">transcoder.dll</TranscoderBinary>
<TranscoderBinary Condition="$(IsOSX) == true">libtranscoder.dylib</TranscoderBinary>
<TranscoderBinary Condition="$(IsLinux) == true">libtranscoder.so</TranscoderBinary>
</PropertyGroup>
<ItemGroup>
@ -22,21 +15,4 @@
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
<ProjectReference Include="../Kyoo.Postgresql/Kyoo.Postgresql.csproj" />
</ItemGroup>
<Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true' And !Exists('$(TranscoderRoot)/build/$(TranscoderBinary)')">
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'" Command="mkdir -p build %26%26 cd build %26%26 cmake .. %26%26 make -j" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' == 'true'" Command="(if not exist build mkdir build) %26%26 cd build %26%26 cmake .. -G &quot;NMake Makefiles&quot; %26%26 nmake" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="An environement capable of building the transcoder was not found. Appropriate tools are not installed, not available in the $PATH or not correctly configured. To fix this you can ether:&#xA; - Fix your tools&#xA; - Skip the transcoder via the '-p:SkipTranscoder=true'&#xA; - Download an already built transcoder and put it in ./Kyoo.Transcoder/build" />
</Target>
<ItemGroup Condition="'$(SkipTranscoder)' != 'true'">
<None Include="$(TranscoderRoot)/build/$(TranscoderBinary)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
</None>
</ItemGroup>
</Project>

View File

@ -1,118 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Runtime.InteropServices;
using Kyoo.Abstractions.Models;
namespace Kyoo.Core.Models.Watch
{
/// <summary>
/// The list of available stream types.
/// Attachments are only used temporarily by the transcoder but are not stored in a database.
/// This is another <see cref="StreamType"/> enum used internally to communicate with ffmpeg.
/// </summary>
public enum FTrackType
{
/// <summary>
/// The type of the stream is not known.
/// </summary>
Unknown = StreamType.Unknown,
/// <summary>
/// The stream is a video.
/// </summary>
Video = StreamType.Video,
/// <summary>
/// The stream is an audio.
/// </summary>
Audio = StreamType.Audio,
/// <summary>
/// The stream is a subtitle.
/// </summary>
Subtitle = StreamType.Subtitle,
/// <summary>
/// The stream is an attachment (a font, an image or something else).
/// Only fonts are handled by kyoo but they are not saved to the database.
/// </summary>
Attachment
}
/// <summary>
/// The unmanaged stream that the transcoder will return.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct FTrack
{
/// <summary>
/// The title of the stream.
/// </summary>
public string Title;
/// <summary>
/// The language of this stream (as a ISO-639-2 language code)
/// </summary>
public string Language;
/// <summary>
/// The codec of this stream.
/// </summary>
public string Codec;
/// <summary>
/// Is this stream the default one of it's type?
/// </summary>
[MarshalAs(UnmanagedType.I1)] public bool IsDefault;
/// <summary>
/// Is this stream tagged as forced?
/// </summary>
[MarshalAs(UnmanagedType.I1)] public bool IsForced;
/// <summary>
/// The path of this track.
/// </summary>
public string Path;
/// <summary>
/// The type of this stream.
/// </summary>
public FTrackType Type;
/// <summary>
/// Create a track from this stream.
/// </summary>
/// <returns>A new track that represent this stream.</returns>
public Track ToTrack()
{
return new()
{
Title = Title,
Language = Language,
Codec = Codec,
IsDefault = IsDefault,
IsForced = IsForced,
Path = Path,
Type = Type < FTrackType.Attachment ? (StreamType)Type : StreamType.Unknown,
IsExternal = false
};
}
}
}

View File

@ -1,46 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
namespace Kyoo.Core.Models.Options
{
/// <summary>
/// The typed list of basic/global options for Kyoo
/// </summary>
public class BasicOptions
{
/// <summary>
/// The path of this list of options
/// </summary>
public const string Path = "Basics";
/// <summary>
/// The temporary folder to cache transmuxed file.
/// </summary>
public string TransmuxPath { get; set; } = "cached/transmux";
/// <summary>
/// The temporary folder to cache transcoded file.
/// </summary>
public string TranscodePath { get; set; } = "cached/transcode";
/// <summary>
/// The path where metadata is stored.
/// </summary>
public string MetadataPath { get; set; } = "metadata/";
}
}

View File

@ -16,6 +16,8 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.IO;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
@ -23,6 +25,7 @@ using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
@ -37,11 +40,6 @@ namespace Kyoo.Core.Api
public class CrudThumbsApi<T> : CrudApi<T>
where T : class, IResource, IThumbnails
{
/// <summary>
/// The file manager used to send images.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// The thumbnail manager used to retrieve images paths.
/// </summary>
@ -53,17 +51,28 @@ namespace Kyoo.Core.Api
/// <param name="repository">
/// The repository to use as a baking store for the type <typeparamref name="T"/>.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public CrudThumbsApi(IRepository<T> repository,
IFileSystem files,
IThumbnailsManager thumbs)
: base(repository)
{
_files = files;
_thumbs = thumbs;
}
/// <summary>
/// Get the content type of a file using it's extension.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
/// <returns>The content type of the file</returns>
private static string _GetContentType(string path)
{
FileExtensionContentTypeProvider provider = new();
if (provider.TryGetContentType(path, out string contentType))
return contentType;
throw new NotImplementedException($"Can't get the content type of the file at: {path}");
}
/// <summary>
/// Get image
/// </summary>
@ -80,11 +89,7 @@ namespace Kyoo.Core.Api
/// <param name="image">The number of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
[HttpGet("{identifier:id}/image-{image:int}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetImage(Identifier identifier, int image)
private async Task<IActionResult> _GetImage(Identifier identifier, int image)
{
T resource = await identifier.Match(
id => Repository.GetOrDefault(id),
@ -92,8 +97,10 @@ namespace Kyoo.Core.Api
);
if (resource == null)
return NotFound();
string path = await _thumbs.GetImagePath(resource, image);
return _files.FileResult(path);
string path = _thumbs.GetImagePath(resource, image);
if (path == null || !System.IO.File.Exists(path))
return NotFound();
return PhysicalFile(Path.GetFullPath(path), _GetContentType(path), true);
}
/// <summary>
@ -113,7 +120,7 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetPoster(Identifier identifier)
{
return GetImage(identifier, Images.Poster);
return _GetImage(identifier, Images.Poster);
}
/// <summary>
@ -133,7 +140,7 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetLogo(Identifier identifier)
{
return GetImage(identifier, Images.Logo);
return _GetImage(identifier, Images.Logo);
}
/// <summary>
@ -151,7 +158,7 @@ namespace Kyoo.Core.Api
[HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)]
public Task<IActionResult> GetBackdrop(Identifier identifier)
{
return GetImage(identifier, Images.Thumbnail);
return _GetImage(identifier, Images.Thumbnail);
}
/// <inheritdoc/>

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

View File

@ -1,55 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Provider"/>.
/// Providers are links to external websites or database.
/// They are mostly linked to plugins that provide metadata from those websites.
/// </summary>
[Route("providers")]
[Route("provider", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Provider))]
[ApiDefinition("Providers", Group = MetadataGroup)]
public class ProviderApi : CrudThumbsApi<Provider>
{
/// <summary>
/// Create a new <see cref="ProviderApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images and fonts.</param>
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public ProviderApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbnails)
: base(libraryManager.ProviderRepository, files, thumbnails)
{ }
}
}

View File

@ -53,12 +53,10 @@ namespace Kyoo.Core.Api
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images and fonts.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public StaffApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs)
: base(libraryManager.PeopleRepository, files, thumbs)
: base(libraryManager.PeopleRepository, thumbs)
{
_libraryManager = libraryManager;
}

View File

@ -51,12 +51,10 @@ namespace Kyoo.Core.Api
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public CollectionApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs)
: base(libraryManager.CollectionRepository, files, thumbs)
: base(libraryManager.CollectionRepository, thumbs)
{
_libraryManager = libraryManager;
}

View File

@ -16,15 +16,12 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
@ -47,43 +44,18 @@ namespace Kyoo.Core.Api
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The transcoder used to retrive fonts.
/// </summary>
private readonly ITranscoder _transcoder;
/// <summary>
/// The file system used to send fonts.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="transcoder">The transcoder used to retrive fonts</param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public EpisodeApi(ILibraryManager libraryManager,
ITranscoder transcoder,
IFileSystem files,
IThumbnailsManager thumbnails)
: base(libraryManager.EpisodeRepository, files, thumbnails)
: base(libraryManager.EpisodeRepository, thumbnails)
{
_libraryManager = libraryManager;
_transcoder = transcoder;
_files = files;
}
/// <inheritdoc/>
public override async Task<ActionResult<Episode>> Create([FromBody] Episode resource)
{
// TODO: Remove this method and use a websocket API to do that.
resource.Tracks = await _transcoder.ExtractInfos(resource, false);
ActionResult<Episode> ret = await base.Create(resource);
return ret;
}
/// <summary>
@ -132,43 +104,5 @@ namespace Kyoo.Core.Api
? NotFound()
: NoContent();
}
/// <summary>
/// Get tracks
/// </summary>
/// <remarks>
/// List the tracks (video, audio and subtitles) available for this episode.
/// This endpoint provide the list of raw tracks, without transcode on it. To get a schema easier to watch
/// on a player, see the [/watch endpoint](#/watch).
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <param name="sortBy">A key to sort tracks by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of tracks to return.</param>
/// <returns>A page of tracks.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No episode with the given ID or slug could be found.</response>
/// TODO fix the /watch endpoint link (when operations ID are specified).
[HttpGet("{identifier:id}/tracks")]
[HttpGet("{identifier:id}/track", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Track>>> GetEpisode(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Track>(x => x.EpisodeID, x => x.Episode.Slug)),
Sort<Track>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Episode>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
}
}

View File

@ -51,12 +51,10 @@ namespace Kyoo.Core.Api
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public SeasonApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs)
: base(libraryManager.SeasonRepository, files, thumbs)
: base(libraryManager.SeasonRepository, thumbs)
{
_libraryManager = libraryManager;
}

View File

@ -48,7 +48,6 @@ namespace Kyoo.Core.Api
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ITranscoder _transcoder;
/// <summary>
/// Create a new <see cref="ShowApi"/>.
@ -56,17 +55,12 @@ namespace Kyoo.Core.Api
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images and fonts.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
/// <param name="transcoder">TODO: Remove this.</param>
public ShowApi(ILibraryManager libraryManager,
IFileSystem files,
IThumbnailsManager thumbs,
ITranscoder transcoder)
: base(libraryManager.ShowRepository, files, thumbs)
IThumbnailsManager thumbs)
: base(libraryManager.ShowRepository, thumbs)
{
_libraryManager = libraryManager;
_transcoder = transcoder;
}
/// <inheritdoc/>
@ -82,7 +76,6 @@ namespace Kyoo.Core.Api
Path = ret.Value.Path
};
episode.Tracks = await _transcoder.ExtractInfos(episode, false);
await _libraryManager.Create(episode);
}
return ret;

View File

@ -1,205 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// An endpoint to retrieve subtitles for a specific episode.
/// </summary>
[Route("subtitles")]
[Route("subtitle", Order = AlternativeRoute)]
[PartialPermission("subtitle")]
[ApiController]
[ApiDefinition("Subtitles", Group = WatchGroup)]
public class SubtitleApi : ControllerBase
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used to send subtitles files.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Create a new <see cref="SubtitleApi"/>.
/// </summary>
/// <param name="libraryManager">The library manager used to interact with the data store.</param>
/// <param name="files">The file manager used to send subtitle files.</param>
public SubtitleApi(ILibraryManager libraryManager, IFileSystem files)
{
_libraryManager = libraryManager;
_files = files;
}
/// <summary>
/// Get subtitle
/// </summary>
/// <remarks>
/// Get the subtitle file with the given identifier.
/// The extension is optional and can be used to ask Kyoo to convert the subtitle file on the fly.
/// </remarks>
/// <param name="identifier">
/// The ID or slug of the subtitle (the same as the corresponding <see cref="Track"/>).
/// </param>
/// <param name="extension">An optional extension for the subtitle file.</param>
/// <returns>The subtitle file</returns>
/// <response code="404">No subtitle exist with the given ID or slug.</response>
[HttpGet("{identifier:int}", Order = AlternativeRoute)]
[HttpGet("{identifier:id}.{extension}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("ReSharper", "RouteTemplates.ParameterTypeAndConstraintsMismatch",
Justification = "An indentifier can be constructed with an int.")]
public async Task<IActionResult> GetSubtitle(Identifier identifier, string extension)
{
Track subtitle = await identifier.Match(
id => _libraryManager.GetOrDefault<Track>(id),
slug =>
{
if (slug.Count(x => x == '.') == 3)
{
int idx = slug.LastIndexOf('.');
extension = slug[(idx + 1)..];
slug = slug[..idx];
}
return _libraryManager.GetOrDefault<Track>(Track.BuildSlug(slug, StreamType.Subtitle));
});
if (subtitle == null)
return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt")
return new ConvertSubripToVtt(subtitle.Path, _files);
return _files.FileResult(subtitle.Path);
}
/// <summary>
/// An action result that convert a subrip subtitle to vtt.
/// </summary>
private class ConvertSubripToVtt : IActionResult
{
/// <summary>
/// The path of the file to convert. It can be any path supported by a <see cref="IFileSystem"/>.
/// </summary>
private readonly string _path;
/// <summary>
/// The file system used to manipulate the given file.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// Create a new <see cref="ConvertSubripToVtt"/>.
/// </summary>
/// <param name="subtitlePath">
/// The path of the subtitle file. It can be any path supported by the given <paramref name="files"/>.
/// </param>
/// <param name="files">
/// The file system used to interact with the file at the given <paramref name="subtitlePath"/>.
/// </param>
public ConvertSubripToVtt(string subtitlePath, IFileSystem files)
{
_path = subtitlePath;
_files = files;
}
/// <inheritdoc />
public async Task ExecuteResultAsync(ActionContext context)
{
List<string> lines = new();
context.HttpContext.Response.StatusCode = 200;
context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt");
await using (StreamWriter writer = new(context.HttpContext.Response.Body))
{
await writer.WriteLineAsync("WEBVTT");
await writer.WriteLineAsync(string.Empty);
await writer.WriteLineAsync(string.Empty);
using StreamReader reader = new(await _files.GetReader(_path));
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line == string.Empty)
{
lines.Add(string.Empty);
IEnumerable<string> processedBlock = _ConvertBlock(lines);
foreach (string t in processedBlock)
await writer.WriteLineAsync(t);
lines.Clear();
}
else
lines.Add(line);
}
}
await context.HttpContext.Response.Body.FlushAsync();
}
/// <summary>
/// Convert a block from subrip to vtt.
/// </summary>
/// <param name="lines">All the lines in the block.</param>
/// <returns>The given block, converted to vtt.</returns>
private static IList<string> _ConvertBlock(IList<string> lines)
{
if (lines.Count < 3)
return lines;
lines[1] = lines[1].Replace(',', '.');
if (lines[2].Length > 5)
{
lines[1] += lines[2].Substring(0, 6) switch
{
"{\\an1}" => " line:93% position:15%",
"{\\an2}" => " line:93%",
"{\\an3}" => " line:93% position:85%",
"{\\an4}" => " line:50% position:15%",
"{\\an5}" => " line:50%",
"{\\an6}" => " line:50% position:85%",
"{\\an7}" => " line:7% position:15%",
"{\\an8}" => " line:7%",
"{\\an9}" => " line:7% position:85%",
_ => " line:93%"
};
}
if (lines[2].StartsWith("{\\an"))
lines[2] = lines[2][6..];
return lines;
}
}
}
}

View File

@ -1,78 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Track"/>.
/// A track contain metadata about a video, an audio or a subtitles.
/// </summary>
[Route("tracks")]
[Route("track", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Track))]
[ApiDefinition("Tracks", Group = WatchGroup)]
public class TrackApi : CrudApi<Track>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="TrackApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
public TrackApi(ILibraryManager libraryManager)
: base(libraryManager.TrackRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get track's episode
/// </summary>
/// <remarks>
/// Get the episode that uses this track.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Track"/>.</param>
/// <returns>The episode that uses this track.</returns>
/// <response code="404">No track with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/episode")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Episode>> GetEpisode(Identifier identifier)
{
return await _libraryManager.Get(identifier.IsContainedIn<Episode, Track>(x => x.Tracks));
}
}
}

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Net.Http;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
@ -46,14 +47,9 @@ namespace Kyoo.Core.Api
private readonly ILibraryManager _libraryManager;
/// <summary>
/// A file system used to retrieve chapters informations.
/// The http client to reach transcoder.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// The transcoder used to list fonts.
/// </summary>
private readonly ITranscoder _transcoder;
private readonly HttpClient _client;
/// <summary>
/// Create a new <see cref="WatchApi"/>.
@ -61,13 +57,11 @@ namespace Kyoo.Core.Api
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="fs">A file system used to retrieve chapters informations.</param>
/// <param name="transcoder">The transcoder used to list fonts.</param>
public WatchApi(ILibraryManager libraryManager, IFileSystem fs, ITranscoder transcoder)
/// <param name="client">The http client to reach transcoder.</param>
public WatchApi(ILibraryManager libraryManager, HttpClient client)
{
_libraryManager = libraryManager;
_files = fs;
_transcoder = transcoder;
_client = client;
}
/// <summary>
@ -91,38 +85,7 @@ namespace Kyoo.Core.Api
);
if (item == null)
return NotFound();
return await WatchItem.FromEpisode(item, _libraryManager, _files, _transcoder);
}
/// <summary>
/// Get font
/// </summary>
/// <remarks>
/// Get a font file that is used in subtitles of this episode.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <param name="slug">The slug of the font to retrieve.</param>
/// <returns>A page of collections.</returns>
/// <response code="404">No show with the given ID/slug could be found or the font does not exist.</response>
[HttpGet("{identifier:id}/fonts/{slug}")]
[HttpGet("{identifier:id}/font/{slug}", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetFont(Identifier identifier, string slug)
{
Episode episode = await identifier.Match(
id => _libraryManager.GetOrDefault<Episode>(id),
slug => _libraryManager.GetOrDefault<Episode>(slug)
);
if (episode == null)
return NotFound();
if (slug.Contains('.'))
slug = slug[..slug.LastIndexOf('.')];
Font font = await _transcoder.GetFont(episode, slug);
if (font == null)
return NotFound();
return _files.FileResult(font.Path);
return await WatchItem.FromEpisode(item, _libraryManager, _client);
}
}
}

View File

@ -1,192 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Autofac.Features.Metadata;
using JetBrains.Annotations;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Kyoo.Host.Controllers
{
/// <summary>
/// A composite that merge every <see cref="IFileSystem"/> available
/// using <see cref="FileSystemMetadataAttribute"/>.
/// </summary>
public class FileSystemComposite : IFileSystem
{
/// <summary>
/// The list of <see cref="IFileSystem"/> mapped to their metadata.
/// </summary>
private readonly ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> _fileSystems;
/// <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>
/// <param name="options">The options to use.</param>
public FileSystemComposite(ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> fileSystems,
IOptionsMonitor<BasicOptions> options)
{
_fileSystems = fileSystems;
_options = options;
}
/// <summary>
/// Retrieve the file system that should be used for a given path.
/// </summary>
/// <param name="path">
/// The path that was requested.
/// </param>
/// <param name="usablePath">
/// The path that the returned file system wants
/// (respecting <see cref="FileSystemMetadataAttribute.StripScheme"/>).
/// </param>
/// <exception cref="ArgumentException">No file system was registered for the given path.</exception>
/// <returns>The file system that should be used for a given path</returns>
[NotNull]
private IFileSystem _GetFileSystemForPath([NotNull] string path, [NotNull] out string usablePath)
{
Regex schemeMatcher = new(@"(.+)://(.*)", RegexOptions.Compiled);
Match match = schemeMatcher.Match(path);
if (!match.Success)
{
usablePath = path;
Meta<Func<IFileSystem>, FileSystemMetadataAttribute> defaultFs = _fileSystems
.SingleOrDefault(x => x.Metadata.Scheme.Contains(string.Empty));
if (defaultFs == null)
throw new ArgumentException($"No file system registered for the default scheme.");
return defaultFs.Value.Invoke();
}
string scheme = match.Groups[1].Value;
Meta<Func<IFileSystem>, FileSystemMetadataAttribute> ret = _fileSystems
.SingleOrDefault(x => x.Metadata.Scheme.Contains(scheme));
if (ret == null)
throw new ArgumentException($"No file system registered for the scheme: {scheme}.");
usablePath = ret.Metadata.StripScheme ? match.Groups[2].Value : path;
return ret.Value.Invoke();
}
/// <inheritdoc />
public IActionResult FileResult(string path, bool rangeSupport = false, string type = null)
{
if (path == null)
return new NotFoundResult();
return _GetFileSystemForPath(path, out string relativePath)
.FileResult(relativePath, rangeSupport, type);
}
/// <inheritdoc />
public Task<Stream> GetReader(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.GetReader(relativePath);
}
/// <inheritdoc />
public Task<Stream> GetReader(string path, AsyncRef<string> mime)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.GetReader(relativePath, mime);
}
/// <inheritdoc />
public Task<Stream> NewFile(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.NewFile(relativePath);
}
/// <inheritdoc />
public Task<string> CreateDirectory(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.CreateDirectory(relativePath);
}
/// <inheritdoc />
public string Combine(params string[] paths)
{
return _GetFileSystemForPath(paths[0], out string relativePath)
.Combine(paths[1..].Prepend(relativePath).ToArray());
}
/// <inheritdoc />
public Task<ICollection<string>> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.ListFiles(relativePath, options);
}
/// <inheritdoc />
public Task<bool> Exists(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return _GetFileSystemForPath(path, out string relativePath)
.Exists(relativePath);
}
/// <inheritdoc />
public Task<string> GetExtraDirectory<T>(T resource)
{
IFileSystem fs = _GetFileSystemForPath(_options.CurrentValue.MetadataPath, out string path);
return fs.GetExtraDirectory(resource);
}
/// <inheritdoc />
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
{
IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _);
return fs.ExtractInfos(episode, reExtract);
}
/// <inheritdoc />
public IActionResult Transmux(Episode episode)
{
IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _);
return fs.Transmux(episode);
}
}
}

View File

@ -20,10 +20,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Models.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Host.Controllers
{
@ -38,11 +36,6 @@ namespace Kyoo.Host.Controllers
/// </summary>
private readonly IServiceProvider _provider;
/// <summary>
/// The configuration to get the plugin's directory.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// The logger used by this class.
/// </summary>
@ -57,14 +50,11 @@ namespace Kyoo.Host.Controllers
/// Create a new <see cref="PluginManager"/> instance.
/// </summary>
/// <param name="provider">A service container to allow initialization of plugins</param>
/// <param name="options">The configuration instance, to get the plugin's directory path.</param>
/// <param name="logger">The logger used by this class.</param>
public PluginManager(IServiceProvider provider,
IOptions<BasicOptions> options,
ILogger<PluginManager> logger)
{
_provider = provider;
_options = options;
_logger = logger;
}

View File

@ -20,11 +20,7 @@ using System.Collections.Generic;
using Autofac;
using Autofac.Extras.AttributeMetadata;
using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Models.Options;
using Kyoo.Host.Controllers;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace Kyoo.Host
@ -42,20 +38,13 @@ namespace Kyoo.Host
/// </summary>
private readonly IPluginManager _plugins;
/// <summary>
/// The configuration used to register options.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// Create a new <see cref="HostModule"/>.
/// </summary>
/// <param name="plugins">The plugin manager that loaded all plugins.</param>
/// <param name="configuration"> The configuration used to register options.</param>
public HostModule(IPluginManager plugins, IConfiguration configuration)
public HostModule(IPluginManager plugins)
{
_plugins = plugins;
_configuration = configuration;
}
/// <inheritdoc />
@ -63,7 +52,6 @@ namespace Kyoo.Host
{
builder.RegisterModule<AttributedMetadataModule>();
builder.RegisterInstance(_plugins).As<IPluginManager>().ExternallyOwned();
builder.RegisterComposite<FileSystemComposite, IFileSystem>().InstancePerLifetimeScope();
}
/// <inheritdoc />

View File

@ -24,7 +24,6 @@ using Autofac;
using Kyoo.Abstractions.Controllers;
using Kyoo.Authentication;
using Kyoo.Core;
using Kyoo.Core.Models.Options;
using Kyoo.Host.Controllers;
using Kyoo.Postgresql;
using Kyoo.Swagger;
@ -35,7 +34,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Host
{
@ -49,11 +47,6 @@ namespace Kyoo.Host
/// </summary>
private readonly IPluginManager _plugins;
/// <summary>
/// The configuration used to register <see cref="IOptions{TOptions}"/> and so on for plugin's specified types.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The plugin that adds controllers and tasks specific to this host.
/// </summary>
@ -63,14 +56,10 @@ namespace Kyoo.Host
/// Created from the DI container, those services are needed to load information and instantiate plugins.s
/// </summary>
/// <param name="plugins">The plugin manager to use to load new plugins and configure the host.</param>
/// <param name="configuration">
/// The configuration used to register <see cref="IOptions{TOptions}"/> and so on for plugin's specified types.
/// </param>
public PluginsStartup(IPluginManager plugins, IConfiguration configuration)
public PluginsStartup(IPluginManager plugins)
{
_plugins = plugins;
_configuration = configuration;
_hostModule = new HostModule(_plugins, configuration);
_hostModule = new HostModule(_plugins);
_plugins.LoadPlugins(
typeof(CoreModule),
typeof(AuthenticationModule),
@ -94,10 +83,9 @@ namespace Kyoo.Host
HostServiceProvider hostProvider = new(host.HostingEnvironment, host.Configuration, logger);
PluginManager plugins = new(
hostProvider,
Options.Create(host.Configuration.GetSection(BasicOptions.Path).Get<BasicOptions>()),
logger.CreateLogger<PluginManager>()
);
return new PluginsStartup(plugins, host.Configuration);
return new PluginsStartup(plugins);
}
/// <summary>

View File

@ -65,11 +65,6 @@ namespace Kyoo.Postgresql
/// </summary>
public DbSet<Episode> Episodes { get; set; }
/// <summary>
/// All tracks of Kyoo. See <see cref="Track"/>.
/// </summary>
public DbSet<Track> Tracks { get; set; }
/// <summary>
/// All genres of Kyoo. See <see cref="Genres"/>.
/// </summary>
@ -268,10 +263,6 @@ namespace Kyoo.Postgresql
.HasMany(x => x.Episodes)
.WithOne(x => x.Season)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Episode>()
.HasMany(x => x.Tracks)
.WithOne(x => x.Episode)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Show>()
.HasOne(x => x.Studio)
@ -307,7 +298,6 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Season>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Episode>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Track>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
@ -344,12 +334,6 @@ namespace Kyoo.Postgresql
modelBuilder.Entity<Episode>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Track>()
.HasIndex(x => new { x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced })
.IsUnique();
modelBuilder.Entity<Track>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<User>()
.HasIndex(x => x.Slug)
.IsUnique();

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
using System;
using System.Collections.Generic;
using Kyoo.Abstractions.Models;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@ -27,6 +28,8 @@ namespace Kyoo.Postgresql.Migrations
/// <summary>
/// The initial migration that build most of the database.
/// </summary>
[DbContext(typeof(PostgresContext))]
[Migration("20210801171613_Initial")]
public partial class Initial : Migration
{
/// <inheritdoc/>
@ -577,7 +580,7 @@ namespace Kyoo.Postgresql.Migrations
is_forced = table.Column<bool>(type: "boolean", nullable: false),
is_external = table.Column<bool>(type: "boolean", nullable: false),
path = table.Column<string>(type: "text", nullable: true),
type = table.Column<StreamType>(type: "stream_type", nullable: false),
type = table.Column<object>(type: "stream_type", nullable: false),
episode_id = table.Column<int>(type: "integer", nullable: false),
track_index = table.Column<int>(type: "integer", nullable: false)
},

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
@ -23,6 +24,8 @@ namespace Kyoo.Postgresql.Migrations
/// <summary>
/// A migration that adds postgres triggers to update slugs.
/// </summary>
[DbContext(typeof(PostgresContext))]
[Migration("20210801171641_Triggers")]
public partial class Triggers : Migration
{
/// <inheritdoc/>

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
@ -23,6 +24,8 @@ namespace Kyoo.Postgresql.Migrations
/// <summary>
/// Remove triggers
/// </summary>
[DbContext(typeof(PostgresContext))]
[Migration("20230724144449_RemoveTriggers")]
public partial class RemoveTriggers : Migration
{
/// <inheritdoc/>

View File

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using System.Collections.Generic;
using Kyoo.Abstractions.Models;
@ -13,8 +13,6 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations
{
[DbContext(typeof(PostgresContext))]
[Migration("20230726100747_Timestamp")]
partial class Timestamp
{
/// <inheritdoc />
@ -545,7 +543,7 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("integer")
.HasColumnName("track_index");
b.Property<StreamType>("Type")
b.Property<object>("Type")
.HasColumnType("stream_type")
.HasColumnName("type");

View File

@ -17,6 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@ -24,6 +25,8 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
[DbContext(typeof(PostgresContext))]
[Migration("20230726100747_Timestamp")]
public partial class Timestamp : Migration
{
/// <inheritdoc />

View File

@ -542,7 +542,7 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("integer")
.HasColumnName("track_index");
b.Property<StreamType>("Type")
b.Property<object>("Type")
.HasColumnType("stream_type")
.HasColumnName("type");

View File

@ -54,7 +54,6 @@ namespace Kyoo.Postgresql
{
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
}
/// <summary>
@ -108,7 +107,6 @@ namespace Kyoo.Postgresql
{
modelBuilder.HasPostgresEnum<Status>();
modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<LibraryItem>()
.ToView("library_items")

View File

@ -48,8 +48,7 @@ namespace Kyoo.Tests.Database
SeasonRepository season = new(_NewContext(), show, provider);
LibraryItemRepository libraryItem = new(_NewContext(),
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
EpisodeRepository episode = new(_NewContext(), show, provider, new Lazy<ITrackRepository>(() => LibraryManager.TrackRepository));
TrackRepository track = new(_NewContext(), episode);
EpisodeRepository episode = new(_NewContext(), show, provider);
UserRepository user = new(_NewContext());
LibraryManager = new LibraryManager(new IBaseRepository[] {
@ -60,7 +59,6 @@ namespace Kyoo.Tests.Database
show,
season,
episode,
track,
people,
studio,
genre,

View File

@ -1,74 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class TrackTests : ATrackTests
{
public TrackTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ATrackTests : RepositoryTests<Track>
{
private readonly ITrackRepository _repository;
protected ATrackTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = repositories.LibraryManager.TrackRepository;
}
[Fact]
public async Task SlugEditTest()
{
await Repositories.LibraryManager.ShowRepository.Edit(new Show
{
ID = 1,
Slug = "new-slug"
}, false);
Track track = await _repository.Get(1);
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
}
[Fact]
public async Task UndefinedLanguageSlugTest()
{
await _repository.Create(new Track
{
ID = 5,
TrackIndex = 0,
Type = StreamType.Video,
Language = null,
EpisodeID = TestSample.Get<Episode>().ID
});
Track track = await _repository.Get(5);
Assert.Equal("anohana-s1e1.und.video", track.Slug);
}
}
}

View File

@ -239,29 +239,6 @@ namespace Kyoo.Tests
ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime()
}
},
{
typeof(Track),
() =>
{
Track ret = new()
{
ID = 1,
EpisodeID = 1,
Codec = "subrip",
Language = "eng",
Path = "/path",
Title = "Subtitle track",
Episode = Get<Episode>(),
Type = StreamType.Subtitle,
IsDefault = true,
IsExternal = false,
IsForced = false,
TrackIndex = 1
};
ret.Episode = null;
return ret;
}
},
{
typeof(People),
() => new People
@ -359,12 +336,6 @@ namespace Kyoo.Tests
episode.Season = season;
context.Episodes.Add(episode);
Track track = Get<Track>();
track.ID = 0;
track.EpisodeID = 0;
track.Episode = episode;
context.Tracks.Add(track);
Studio studio = Get<Studio>();
studio.ID = 0;
studio.Shows = new List<Show> { show };

View File

@ -20,13 +20,17 @@
import { z } from "zod";
import { zdate } from "../utils";
import { ResourceP, ImagesP, imageFn } from "../traits";
import { ImagesP, imageFn } from "../traits";
import { EpisodeP } from "./episode";
/**
* A video, audio or subtitle track for an episode.
* A audio or subtitle track.
*/
export const TrackP = ResourceP.extend({
export const TrackP = z.object({
/**
* The index of this track on the episode.
*/
index: z.number(),
/**
* The title of the stream.
*/
@ -47,44 +51,16 @@ export const TrackP = ResourceP.extend({
* Is this stream tagged as forced?
*/
isForced: z.boolean(),
/**
* Is this track extern to the episode's file?
*/
isExternal: z.boolean(),
/**
* The index of this track on the episode.
*/
trackIndex: z.number(),
/**
* A user-friendly name for this track. It does not include the track type.
*/
displayName: z.string(),
});
export type Audio = z.infer<typeof TrackP>;
export const SubtitleP = TrackP.extend({
/*
* The url of this track (only if this is a subtitle)..
*/
link: z.string().transform(imageFn).nullable(),
});
export type Track = z.infer<typeof TrackP>;
export const FontP = z.object({
/*
* A human-readable identifier, used in the URL.
*/
slug: z.string(),
/*
* The name of the font file (with the extension).
*/
file: z.string(),
/*
* The format of this font (the extension).
*/
format: z.string(),
/*
* The url of the font.
*/
link: z.string().transform(imageFn),
});
export type Font = z.infer<typeof FontP>;
export type Subtitle = z.infer<typeof SubtitleP>;
export const ChapterP = z.object({
/**
@ -128,31 +104,41 @@ const WatchMovieP = z.preprocess(
* The release date of this episode. It can be null if unknown.
*/
releaseDate: zdate().nullable(),
/**
* The container of the video file of this episode. Common containers are mp4, mkv, avi and so
* on.
* The transcoder's info for this item. This include subtitles, fonts, chapters...
*/
container: z.string(),
/**
* The video track. See Track for more information.
*/
video: TrackP,
/**
* The list of audio tracks. See Track for more information.
*/
audios: z.array(TrackP),
/**
* The list of subtitles tracks. See Track for more information.
*/
subtitles: z.array(TrackP),
/**
* The list of fonts that can be used to display subtitles.
*/
fonts: z.array(FontP),
/**
* The list of chapters. See Chapter for more information.
*/
chapters: z.array(ChapterP),
info: z.object({
/**
* The sha1 of the video file.
*/
sha: z.string(),
/**
* The internal path of the video file.
*/
path: z.string(),
/**
* The container of the video file of this episode. Common containers are mp4, mkv, avi and so
* on.
*/
container: z.string(),
/**
* The list of audio tracks.
*/
audios: z.array(TrackP),
/**
* The list of subtitles tracks.
*/
subtitles: z.array(SubtitleP),
/**
* The list of fonts that can be used to display subtitles.
*/
fonts: z.array(z.string().transform(imageFn)),
/**
* The list of chapters. See Chapter for more information.
*/
chapters: z.array(ChapterP),
}),
/**
* The links to the videos of this watch item.
*/

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Font, Track, WatchItem } from "@kyoo/models";
import { Subtitle, WatchItem } from "@kyoo/models";
import { IconButton, tooltip, Menu, ts } from "@kyoo/primitives";
import { useAtom } from "jotai";
import { Platform, View } from "react-native";
@ -40,8 +40,8 @@ export const RightButtons = ({
onMenuClose,
...props
}: {
subtitles?: Track[];
fonts?: Font[];
subtitles?: Subtitle[];
fonts?: string[];
qualities?: WatchItem["link"];
onMenuOpen: () => void;
onMenuClose: () => void;
@ -71,7 +71,7 @@ export const RightButtons = ({
/>
{subtitles.map((x) => (
<Menu.Item
key={x.id}
key={x.index}
label={x.displayName}
selected={selectedSubtitle === x}
onSelect={() => setSubtitle(x)}

View File

@ -51,9 +51,9 @@ const mapData = (
href: data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "#",
poster: data.poster,
qualities: data.link,
subtitles: data.subtitles,
chapters: data.chapters,
fonts: data.fonts,
subtitles: data.info.subtitles,
chapters: data.info.chapters,
fonts: data.info.fonts,
previousSlug,
nextSlug,
};
@ -92,7 +92,7 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
const next =
data && !data.isMovie && data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : undefined;
useVideoKeyboard(data?.subtitles, data?.fonts, previous, next);
useVideoKeyboard(data?.info.subtitles, data?.info.fonts, previous, next);
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
const [isPlaying, setPlay] = useAtom(playAtom);
@ -191,9 +191,9 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
>
<Video
links={data?.link}
subtitles={data?.subtitles}
subtitles={data?.info.subtitles}
setError={setPlaybackError}
fonts={data?.fonts}
fonts={data?.info.fonts}
onPointerDown={(e) => onPointerDown(e)}
onEnd={() => {
if (!data) return;

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Font, getToken, Track } from "@kyoo/models";
import { getToken, Subtitle } from "@kyoo/models";
import {
forwardRef,
RefObject,
@ -201,7 +201,7 @@ export default Video;
let htmlTrack: HTMLTrackElement | null;
let subOcto: SubtitleOctopus | null;
const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Track | null, fonts?: Font[]) => {
const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Subtitle | null, fonts?: string[]) => {
useEffect(() => {
if (!player.current) return;
@ -228,7 +228,7 @@ const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Track | null, f
track.kind = "subtitles";
track.label = value.displayName;
if (value.language) track.srclang = value.language;
track.src = value.link! + ".vtt";
track.src = value.link;
track.className = "subtitle_container";
track.default = true;
track.onload = () => {
@ -243,12 +243,11 @@ const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Track | null, f
removeOctoSub();
subOcto = new SubtitleOctopus({
video: player.current,
subUrl: value.link!,
subUrl: value.link,
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
fallbackFont: "/default.woff2",
fonts: fonts?.map((x) => x.link),
// availableFonts: fonts ? Object.fromEntries(fonts.map((x) => [x.slug, x.link])) : undefined,
fonts: fonts,
// lazyFileLoading: true,
renderMode: "wasm-blend",
});

View File

@ -14,11 +14,13 @@ use crate::transcode::Quality;
#[derive(Serialize, ToSchema)]
pub struct MediaInfo {
/// The sha1 of the video file.
pub sha: String,
/// The internal path of this track.
/// The internal path of the video file.
pub path: String,
/// The length of the media in seconds.
pub length: f32,
/// The container of the video file of this episode.
pub container: String,
pub video: Video,
pub audios: Vec<Audio>,
@ -54,9 +56,9 @@ pub struct Audio {
/// The codec of this stream.
pub codec: String,
/// Is this stream the default one of it's type?
pub default: bool,
pub is_default: bool,
/// Is this stream tagged as forced? (useful only for subtitles)
pub forced: bool,
pub is_forced: bool,
}
#[derive(Serialize, ToSchema)]
@ -70,9 +72,9 @@ pub struct Subtitle {
/// The codec of this stream.
pub codec: String,
/// Is this stream the default one of it's type?
pub default: bool,
pub is_default: bool,
/// Is this stream tagged as forced? (useful only for subtitles)
pub forced: bool,
pub is_forced: bool,
/// The link to access this subtitle.
pub link: String,
}
@ -80,9 +82,9 @@ pub struct Subtitle {
#[derive(Serialize, ToSchema)]
pub struct Chapter {
/// The start time of the chapter (in second from the start of the episode).
pub start: f32,
pub start_time: f32,
/// The end time of the chapter (in second from the start of the episode).
pub end: f32,
pub end_time: f32,
/// The name of this chapter. This should be a human-readable name that could be presented to the user.
pub name: String, // TODO: add a type field for Opening, Credits...
}
@ -149,8 +151,8 @@ pub async fn identify(path: String) -> Result<MediaInfo, std::io::Error> {
title: a["Title"].as_str().map(|x| x.to_string()),
language: a["Language"].as_str().map(|x| x.to_string()),
codec,
default: a["Default"] == "Yes",
forced: a["Forced"] == "No",
is_default: a["Default"] == "Yes",
is_forced: a["Forced"] == "No",
}
})
.collect();
@ -193,8 +195,8 @@ pub async fn identify(path: String) -> Result<MediaInfo, std::io::Error> {
language: a["Language"].as_str().map(|x| x.to_string()),
// TODO: format is invalid. Channels count missing...
codec: a["Format"].as_str().unwrap().to_string(),
default: a["Default"] == "Yes",
forced: a["Forced"] == "No",
is_default: a["Default"] == "Yes",
is_forced: a["Forced"] == "No",
})
.collect(),
subtitles: subs,
@ -209,8 +211,8 @@ pub async fn identify(path: String) -> Result<MediaInfo, std::io::Error> {
.map(|x| {
std::iter::zip(x["extra"].entries(), x["extra"].entries().skip(1))
.map(|((start, name), (end, _))| Chapter {
start: time_to_seconds(start),
end: time_to_seconds(end),
start_time: time_to_seconds(start),
end_time: time_to_seconds(end),
name: name.as_str().unwrap().to_string(),
})
.collect()