mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Handling tracks slugs
This commit is contained in:
parent
6bd7b47fd9
commit
dc42ed031f
2
.github/workflows/analysis.yml
vendored
2
.github/workflows/analysis.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
check-name: tests
|
check-name: tests
|
||||||
repo-token: ${{secrets.GITHUB_TOKEN}}
|
repo-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
running-workflow-name: analysis
|
running-workflow-name: analysis
|
||||||
allowed-conclusions: success,skipped,cancelled,neutral,failed
|
allowed-conclusions: success,skipped,cancelled,neutral,failure
|
||||||
- name: Download coverage report
|
- name: Download coverage report
|
||||||
uses: dawidd6/action-download-artifact@v2
|
uses: dawidd6/action-download-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -29,8 +29,10 @@ jobs:
|
|||||||
POSTGRES_USERNAME: postgres
|
POSTGRES_USERNAME: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
- name: Sanitize coverage output
|
- name: Sanitize coverage output
|
||||||
|
if: ${{ always() }}
|
||||||
run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml
|
run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: coverage.xml
|
name: coverage.xml
|
||||||
|
@ -149,16 +149,6 @@ namespace Kyoo.Controllers
|
|||||||
[ItemNotNull]
|
[ItemNotNull]
|
||||||
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
|
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a track from it's slug and it's type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="slug">The slug of the track</param>
|
|
||||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
|
||||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
|
||||||
/// <returns>The track found</returns>
|
|
||||||
[ItemNotNull]
|
|
||||||
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the resource by it's ID or null if it is not found.
|
/// Get the resource by it's ID or null if it is not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -224,15 +214,6 @@ namespace Kyoo.Controllers
|
|||||||
[ItemCanBeNull]
|
[ItemCanBeNull]
|
||||||
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
|
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a track from it's slug and it's type or null if it is not found.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="slug">The slug of the track</param>
|
|
||||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
|
||||||
/// <returns>The track found</returns>
|
|
||||||
[ItemCanBeNull]
|
|
||||||
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load a related resource
|
/// Load a related resource
|
||||||
|
@ -375,25 +375,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A repository to handle tracks
|
/// A repository to handle tracks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ITrackRepository : IRepository<Track>
|
public interface ITrackRepository : IRepository<Track> { }
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get a track from it's slug and it's type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="slug">The slug of the track</param>
|
|
||||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
|
||||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
|
||||||
/// <returns>The track found</returns>
|
|
||||||
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a track from it's slug and it's type or null if it is not found.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="slug">The slug of the track</param>
|
|
||||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
|
||||||
/// <returns>The track found</returns>
|
|
||||||
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A repository to handle libraries.
|
/// A repository to handle libraries.
|
||||||
|
@ -114,12 +114,6 @@ namespace Kyoo.Controllers
|
|||||||
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
|
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
|
|
||||||
{
|
|
||||||
return TrackRepository.Get(slug, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<T> GetOrDefault<T>(int id)
|
public async Task<T> GetOrDefault<T>(int id)
|
||||||
where T : class, IResource
|
where T : class, IResource
|
||||||
@ -165,12 +159,6 @@ namespace Kyoo.Controllers
|
|||||||
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
|
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
|
|
||||||
{
|
|
||||||
return await TrackRepository.GetOrDefault(slug, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
|
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
|
||||||
where T : class, IResource
|
where T : class, IResource
|
||||||
|
@ -33,42 +33,27 @@ namespace Kyoo.Models
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
string type = Type switch
|
string type = Type.ToString().ToLower();
|
||||||
{
|
|
||||||
StreamType.Subtitle => "",
|
|
||||||
StreamType.Video => "video.",
|
|
||||||
StreamType.Audio => "audio.",
|
|
||||||
StreamType.Attachment => "font.",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
||||||
string codec = Codec switch
|
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
|
||||||
{
|
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
|
||||||
"subrip" => ".srt",
|
|
||||||
{} x => $".{x}"
|
|
||||||
};
|
|
||||||
return $"{EpisodeSlug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}";
|
|
||||||
}
|
}
|
||||||
[UsedImplicitly] private set
|
[UsedImplicitly] private set
|
||||||
{
|
{
|
||||||
Match match = Regex.Match(value, @"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)"
|
if (value == null)
|
||||||
+ @"(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..\w)?");
|
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)
|
if (!match.Success)
|
||||||
{
|
throw new ArgumentException("Invalid track slug. " +
|
||||||
match = Regex.Match(value, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-forced)?(\..\w)?");
|
"Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]");
|
||||||
if (!match.Success)
|
|
||||||
throw new ArgumentException("Invalid track slug. " +
|
|
||||||
"Format: {episodeSlug}.{language}[-forced][.{extension}]");
|
|
||||||
}
|
|
||||||
|
|
||||||
EpisodeSlug = Episode.GetSlug(match.Groups["show"].Value,
|
EpisodeSlug = match.Groups["ep"].Value;
|
||||||
match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null,
|
Language = match.Groups["lang"].Value;
|
||||||
match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null);
|
TrackIndex = int.Parse(match.Groups["index"].Value);
|
||||||
Language = match.Groups["language"].Value;
|
|
||||||
IsForced = match.Groups["forced"].Success;
|
IsForced = match.Groups["forced"].Success;
|
||||||
if (match.Groups["type"].Success)
|
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
||||||
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,5 +152,32 @@ namespace Kyoo.Models
|
|||||||
_ => mkvLanguage
|
_ => mkvLanguage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseSlug">The slug to edit</param>
|
||||||
|
/// <param name="type">The new type of this </param>
|
||||||
|
/// <param name="language"></param>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
/// <param name="forced"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditSlug(string baseSlug,
|
||||||
|
StreamType type = StreamType.Unknown,
|
||||||
|
string language = null,
|
||||||
|
int? index = null,
|
||||||
|
bool? forced = null)
|
||||||
|
{
|
||||||
|
Track track = new() {Slug = baseSlug};
|
||||||
|
if (type != StreamType.Unknown)
|
||||||
|
track.Type = type;
|
||||||
|
if (language != null)
|
||||||
|
track.Language = language;
|
||||||
|
if (index != null)
|
||||||
|
track.TrackIndex = index.Value;
|
||||||
|
if (forced != null)
|
||||||
|
track.IsForced = forced.Value;
|
||||||
|
return track.Slug;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -330,8 +330,6 @@ namespace Kyoo
|
|||||||
modelBuilder.Entity<Track>()
|
modelBuilder.Entity<Track>()
|
||||||
.Property(x => x.Slug)
|
.Property(x => x.Slug)
|
||||||
.ValueGeneratedOnAddOrUpdate();
|
.ValueGeneratedOnAddOrUpdate();
|
||||||
|
|
||||||
// modelBuilder.Ignore<LibraryItem>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -502,52 +500,6 @@ namespace Kyoo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save items or retry with a custom method if a duplicate is found.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
|
|
||||||
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
|
|
||||||
/// The second parameter is the current retry number.</param>
|
|
||||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
|
||||||
/// <typeparam name="T">The type of the item to save</typeparam>
|
|
||||||
/// <returns>The number of state entries written to the database.</returns>
|
|
||||||
public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new())
|
|
||||||
{
|
|
||||||
return SaveOrRetry(obj, onFail, 0, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save items or retry with a custom method if a duplicate is found.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
|
|
||||||
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
|
|
||||||
/// The second parameter is the current retry number.</param>
|
|
||||||
/// <param name="recurse">The current retry number.</param>
|
|
||||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
|
||||||
/// <typeparam name="T">The type of the item to save</typeparam>
|
|
||||||
/// <returns>The number of state entries written to the database.</returns>
|
|
||||||
private async Task<T> SaveOrRetry<T>(T obj,
|
|
||||||
Func<T, int, T> onFail,
|
|
||||||
int recurse,
|
|
||||||
CancellationToken cancellationToken = new())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await base.SaveChangesAsync(true, cancellationToken);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
catch (DbUpdateException ex) when (IsDuplicateException(ex))
|
|
||||||
{
|
|
||||||
recurse++;
|
|
||||||
return await SaveOrRetry(onFail(obj, recurse), onFail, recurse, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException)
|
|
||||||
{
|
|
||||||
DiscardChanges();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if the exception is a duplicated exception.
|
/// Check if the exception is a duplicated exception.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -13,7 +13,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
LANGUAGE PLPGSQL
|
LANGUAGE PLPGSQL
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
NEW.slug := CONCAT(
|
NEW.slug := CONCAT(
|
||||||
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
||||||
'-s',
|
'-s',
|
||||||
NEW.season_number
|
NEW.season_number
|
||||||
@ -38,10 +38,10 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
NEW.slug := CONCAT(
|
NEW.slug := CONCAT(
|
||||||
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
||||||
CASE
|
CASE
|
||||||
WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL
|
WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL
|
||||||
WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number)
|
WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number)
|
||||||
ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number)
|
ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number)
|
||||||
END
|
END
|
||||||
);
|
);
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END
|
END
|
||||||
@ -63,20 +63,80 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
BEGIN
|
BEGIN
|
||||||
UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id;
|
UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id;
|
||||||
UPDATE episodes SET slug = CASE
|
UPDATE episodes SET slug = CASE
|
||||||
WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug
|
WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug
|
||||||
WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number)
|
WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number)
|
||||||
ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number)
|
ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number)
|
||||||
END
|
END WHERE show_id = NEW.id;
|
||||||
WHERE show_id = NEW.id;
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END
|
END
|
||||||
$$;");
|
$$;");
|
||||||
|
|
||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
migrationBuilder.Sql(@"
|
migrationBuilder.Sql(@"
|
||||||
CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows
|
CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows
|
||||||
FOR EACH ROW EXECUTE PROCEDURE show_slug_update();");
|
FOR EACH ROW EXECUTE PROCEDURE show_slug_update();");
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION episode_update_tracks_slug()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE tracks SET slug = CONCAT(
|
||||||
|
NEW.slug,
|
||||||
|
'.', language,
|
||||||
|
CASE (track_index)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE CONCAT('-', track_index)
|
||||||
|
END,
|
||||||
|
CASE (is_forced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END,
|
||||||
|
'.', type
|
||||||
|
) WHERE episode_id = NEW.id;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER episode_track_slug_trigger AFTER UPDATE OF slug ON episodes
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE episode_update_tracks_slug();");
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION track_slug_update()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.track_index = 0 THEN
|
||||||
|
NEW.track_index := (SELECT COUNT(*) FROM tracks
|
||||||
|
WHERE episode_id = NEW.episode_id AND type = NEW.type
|
||||||
|
AND language = NEW.language AND is_forced = NEW.is_forced);
|
||||||
|
END IF;
|
||||||
|
NEW.slug := CONCAT(
|
||||||
|
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
|
||||||
|
'.', NEW.language,
|
||||||
|
CASE (NEW.track_index)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE CONCAT('-', NEW.track_index)
|
||||||
|
END,
|
||||||
|
CASE (NEW.is_forced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END,
|
||||||
|
'.', NEW.type
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER track_slug_trigger
|
||||||
|
BEFORE INSERT OR UPDATE OF episode_id, is_forced, language, track_index, type ON tracks
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE track_slug_update();");
|
||||||
|
|
||||||
|
|
||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
migrationBuilder.Sql(@"
|
migrationBuilder.Sql(@"
|
||||||
@ -112,6 +172,14 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;");
|
migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;");
|
||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER track_slug_trigger ON tracks;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION track_slug_update;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER episode_track_slug_trigger ON episodes;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION episode_update_tracks_slug;");
|
||||||
|
// language=PostgreSQL
|
||||||
migrationBuilder.Sql(@"DROP VIEW library_items;");
|
migrationBuilder.Sql(@"DROP VIEW library_items;");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,13 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
migrationBuilder.Sql(@"
|
migrationBuilder.Sql(@"
|
||||||
CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW
|
CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE Episodes
|
UPDATE Episodes
|
||||||
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||||
CASE
|
CASE
|
||||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||||
END
|
END
|
||||||
WHERE ID == new.ID;
|
WHERE ID == new.ID;
|
||||||
END");
|
END");
|
||||||
// language=SQLite
|
// language=SQLite
|
||||||
@ -39,29 +39,114 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID
|
CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID
|
||||||
ON Episodes FOR EACH ROW
|
ON Episodes FOR EACH ROW
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE Episodes
|
UPDATE Episodes
|
||||||
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||||
CASE
|
CASE
|
||||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||||
END
|
END
|
||||||
WHERE ID == new.ID;
|
WHERE ID == new.ID;
|
||||||
END");
|
END");
|
||||||
|
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER TrackSlugInsert
|
||||||
|
AFTER INSERT ON Tracks
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Tracks SET TrackIndex = (
|
||||||
|
SELECT COUNT(*) FROM Tracks
|
||||||
|
WHERE EpisodeID = new.EpisodeID AND Type = new.Type
|
||||||
|
AND Language = new.Language AND IsForced = new.IsForced
|
||||||
|
) WHERE ID = new.ID AND TrackIndex = 0;
|
||||||
|
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
||||||
|
'.' || Language ||
|
||||||
|
CASE (TrackIndex)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE '-' || (TrackIndex)
|
||||||
|
END ||
|
||||||
|
CASE (IsForced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END ||
|
||||||
|
CASE (Type)
|
||||||
|
WHEN 1 THEN '.video'
|
||||||
|
WHEN 2 THEN '.audio'
|
||||||
|
WHEN 3 THEN '.subtitle'
|
||||||
|
ELSE '.' || Type
|
||||||
|
END
|
||||||
|
WHERE ID = new.ID;
|
||||||
|
END;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER TrackSlugUpdate
|
||||||
|
AFTER UPDATE OF EpisodeID, IsForced, Language, TrackIndex, Type ON Tracks
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Tracks SET TrackIndex = (
|
||||||
|
SELECT COUNT(*) FROM Tracks
|
||||||
|
WHERE EpisodeID = new.EpisodeID AND Type = new.Type
|
||||||
|
AND Language = new.Language AND IsForced = new.IsForced
|
||||||
|
) WHERE ID = new.ID AND TrackIndex = 0;
|
||||||
|
UPDATE Tracks SET Slug =
|
||||||
|
(SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
||||||
|
'.' || Language ||
|
||||||
|
CASE (TrackIndex)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE '-' || (TrackIndex)
|
||||||
|
END ||
|
||||||
|
CASE (IsForced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END ||
|
||||||
|
CASE (Type)
|
||||||
|
WHEN 1 THEN '.video'
|
||||||
|
WHEN 2 THEN '.audio'
|
||||||
|
WHEN 3 THEN '.subtitle'
|
||||||
|
ELSE '.' || Type
|
||||||
|
END
|
||||||
|
WHERE ID = new.ID;
|
||||||
|
END;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER EpisodeUpdateTracksSlug
|
||||||
|
AFTER UPDATE OF Slug ON Episodes
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Tracks SET Slug =
|
||||||
|
NEW.Slug ||
|
||||||
|
'.' || Language ||
|
||||||
|
CASE (TrackIndex)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE '-' || TrackIndex
|
||||||
|
END ||
|
||||||
|
CASE (IsForced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END ||
|
||||||
|
CASE (Type)
|
||||||
|
WHEN 1 THEN '.video'
|
||||||
|
WHEN 2 THEN '.audio'
|
||||||
|
WHEN 3 THEN '.subtitle'
|
||||||
|
ELSE '.' || Type
|
||||||
|
END
|
||||||
|
WHERE EpisodeID = NEW.ID;
|
||||||
|
END;");
|
||||||
|
|
||||||
|
|
||||||
// language=SQLite
|
// language=SQLite
|
||||||
migrationBuilder.Sql(@"
|
migrationBuilder.Sql(@"
|
||||||
CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW
|
CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID;
|
UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID;
|
||||||
UPDATE Episodes
|
UPDATE Episodes
|
||||||
SET Slug = new.Slug ||
|
SET Slug = new.Slug ||
|
||||||
CASE
|
CASE
|
||||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||||
END
|
END
|
||||||
WHERE ShowID = new.ID;
|
WHERE ShowID = new.ID;
|
||||||
END;");
|
END;");
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -34,5 +35,17 @@ namespace Kyoo.Tests.Library
|
|||||||
{
|
{
|
||||||
_repository = repositories.LibraryManager.TrackRepository;
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -127,7 +127,7 @@ namespace Kyoo.Controllers
|
|||||||
|
|
||||||
if (changed.Tracks != null || resetOld)
|
if (changed.Tracks != null || resetOld)
|
||||||
{
|
{
|
||||||
await Database.Entry(resource).Collection(x => x.Tracks).LoadAsync();
|
await _tracks.DeleteAll(x => x.EpisodeID == resource.ID);
|
||||||
resource.Tracks = changed.Tracks;
|
resource.Tracks = changed.Tracks;
|
||||||
await ValidateTracks(resource);
|
await ValidateTracks(resource);
|
||||||
}
|
}
|
||||||
@ -148,14 +148,10 @@ namespace Kyoo.Controllers
|
|||||||
/// <returns>The <see cref="resource"/> parameter is returned.</returns>
|
/// <returns>The <see cref="resource"/> parameter is returned.</returns>
|
||||||
private async Task<Episode> ValidateTracks(Episode resource)
|
private async Task<Episode> ValidateTracks(Episode resource)
|
||||||
{
|
{
|
||||||
resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.MapAsync((x, i) =>
|
resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.SelectAsync(x =>
|
||||||
{
|
{
|
||||||
x.Episode = resource;
|
x.Episode = resource;
|
||||||
// TODO use a trigger for the next line.
|
x.EpisodeSlug = resource.Slug;
|
||||||
x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language
|
|
||||||
&& x.IsForced == y.IsForced
|
|
||||||
&& x.Codec == y.Codec
|
|
||||||
&& x.Type == y.Type);
|
|
||||||
return _tracks.Create(x);
|
return _tracks.Create(x);
|
||||||
}).ToListAsync());
|
}).ToListAsync());
|
||||||
return resource;
|
return resource;
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Kyoo.Models.Exceptions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
@ -34,56 +31,6 @@ namespace Kyoo.Controllers
|
|||||||
_database = database;
|
_database = database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<Track> IRepository<Track>.Get(string slug)
|
|
||||||
{
|
|
||||||
return Get(slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
|
|
||||||
{
|
|
||||||
Track ret = await GetOrDefault(slug, type);
|
|
||||||
if (ret == null)
|
|
||||||
throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}.");
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
|
|
||||||
{
|
|
||||||
Match match = Regex.Match(slug,
|
|
||||||
@"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
|
|
||||||
|
|
||||||
if (!match.Success)
|
|
||||||
{
|
|
||||||
if (int.TryParse(slug, out int id))
|
|
||||||
return GetOrDefault(id);
|
|
||||||
match = Regex.Match(slug, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
|
|
||||||
if (!match.Success)
|
|
||||||
throw new ArgumentException("Invalid track slug. " +
|
|
||||||
"Format: {episodeSlug}.{language}[-forced][.{extension}]");
|
|
||||||
}
|
|
||||||
|
|
||||||
string showSlug = match.Groups["show"].Value;
|
|
||||||
int? seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null;
|
|
||||||
int? episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null;
|
|
||||||
string language = match.Groups["language"].Value;
|
|
||||||
bool forced = match.Groups["forced"].Success;
|
|
||||||
if (match.Groups["type"].Success)
|
|
||||||
type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
|
||||||
|
|
||||||
IQueryable<Track> query = _database.Tracks.Where(x => x.Episode.Show.Slug == showSlug
|
|
||||||
&& x.Episode.SeasonNumber == seasonNumber
|
|
||||||
&& x.Episode.EpisodeNumber == episodeNumber
|
|
||||||
&& x.Language == language
|
|
||||||
&& x.IsForced == forced);
|
|
||||||
if (type != StreamType.Unknown)
|
|
||||||
return query.FirstOrDefaultAsync(x => x.Type == type);
|
|
||||||
return query.FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<ICollection<Track>> Search(string query)
|
public override Task<ICollection<Track>> Search(string query)
|
||||||
{
|
{
|
||||||
@ -93,6 +40,9 @@ namespace Kyoo.Controllers
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<Track> Create(Track obj)
|
public override async Task<Track> Create(Track obj)
|
||||||
{
|
{
|
||||||
|
if (obj == null)
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
|
||||||
if (obj.EpisodeID <= 0)
|
if (obj.EpisodeID <= 0)
|
||||||
{
|
{
|
||||||
obj.EpisodeID = obj.Episode?.ID ?? 0;
|
obj.EpisodeID = obj.Episode?.ID ?? 0;
|
||||||
@ -102,14 +52,7 @@ namespace Kyoo.Controllers
|
|||||||
|
|
||||||
await base.Create(obj);
|
await base.Create(obj);
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
_database.Entry(obj).State = EntityState.Added;
|
||||||
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
|
await _database.SaveChangesAsync();
|
||||||
await _database.SaveOrRetry(obj, (x, i) =>
|
|
||||||
{
|
|
||||||
if (i > 10)
|
|
||||||
throw new DuplicatedItemException($"More than 10 same tracks exists {x.Slug}. Aborting...");
|
|
||||||
x.TrackIndex++;
|
|
||||||
return x;
|
|
||||||
});
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using Kyoo.Controllers;
|
|||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Kyoo.Models.Options;
|
using Kyoo.Models.Options;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Kyoo.SqLite;
|
|
||||||
using Kyoo.Tasks;
|
using Kyoo.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Kyoo.Models;
|
||||||
using Kyoo.Models;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -27,19 +26,9 @@ namespace Kyoo.Api
|
|||||||
[Permission(nameof(SubtitleApi), Kind.Read)]
|
[Permission(nameof(SubtitleApi), Kind.Read)]
|
||||||
public async Task<IActionResult> GetSubtitle(string slug, string extension)
|
public async Task<IActionResult> GetSubtitle(string slug, string extension)
|
||||||
{
|
{
|
||||||
Track subtitle;
|
Track subtitle = await _libraryManager.GetOrDefault<Track>(Track.EditSlug(slug, StreamType.Subtitle));
|
||||||
try
|
if (subtitle == null)
|
||||||
{
|
|
||||||
subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle);
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new {error = ex.Message});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subtitle is not {Type: StreamType.Subtitle})
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
if (subtitle.Codec == "subrip" && extension == "vtt")
|
if (subtitle.Codec == "subrip" && extension == "vtt")
|
||||||
return new ConvertSubripToVtt(subtitle.Path, _files);
|
return new ConvertSubripToVtt(subtitle.Path, _files);
|
||||||
return _files.FileResult(subtitle.Path);
|
return _files.FileResult(subtitle.Path);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user