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
|
||||
repo-token: ${{secrets.GITHUB_TOKEN}}
|
||||
running-workflow-name: analysis
|
||||
allowed-conclusions: success,skipped,cancelled,neutral,failed
|
||||
allowed-conclusions: success,skipped,cancelled,neutral,failure
|
||||
- name: Download coverage report
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -29,8 +29,10 @@ jobs:
|
||||
POSTGRES_USERNAME: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
- name: Sanitize coverage output
|
||||
if: ${{ always() }}
|
||||
run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml
|
||||
- name: Upload coverage report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage.xml
|
||||
|
@ -149,16 +149,6 @@ namespace Kyoo.Controllers
|
||||
[ItemNotNull]
|
||||
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>
|
||||
/// Get the resource by it's ID or null if it is not found.
|
||||
/// </summary>
|
||||
@ -224,15 +214,6 @@ namespace Kyoo.Controllers
|
||||
[ItemCanBeNull]
|
||||
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>
|
||||
/// Load a related resource
|
||||
|
@ -375,25 +375,7 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// A repository to handle tracks
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
public interface ITrackRepository : IRepository<Track> { }
|
||||
|
||||
/// <summary>
|
||||
/// A repository to handle libraries.
|
||||
|
@ -114,12 +114,6 @@ namespace Kyoo.Controllers
|
||||
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
|
||||
{
|
||||
return TrackRepository.Get(slug, type);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> GetOrDefault<T>(int id)
|
||||
where T : class, IResource
|
||||
@ -165,12 +159,6 @@ namespace Kyoo.Controllers
|
||||
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 />
|
||||
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
|
||||
where T : class, IResource
|
||||
|
@ -33,42 +33,27 @@ namespace Kyoo.Models
|
||||
{
|
||||
get
|
||||
{
|
||||
string type = Type switch
|
||||
{
|
||||
StreamType.Subtitle => "",
|
||||
StreamType.Video => "video.",
|
||||
StreamType.Audio => "audio.",
|
||||
StreamType.Attachment => "font.",
|
||||
_ => ""
|
||||
};
|
||||
string type = Type.ToString().ToLower();
|
||||
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
||||
string codec = Codec switch
|
||||
{
|
||||
"subrip" => ".srt",
|
||||
{} x => $".{x}"
|
||||
};
|
||||
return $"{EpisodeSlug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}";
|
||||
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
|
||||
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
|
||||
}
|
||||
[UsedImplicitly] private set
|
||||
{
|
||||
Match match = Regex.Match(value, @"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)"
|
||||
+ @"(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..\w)?");
|
||||
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)
|
||||
{
|
||||
match = Regex.Match(value, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-forced)?(\..\w)?");
|
||||
if (!match.Success)
|
||||
throw new ArgumentException("Invalid track slug. " +
|
||||
"Format: {episodeSlug}.{language}[-forced][.{extension}]");
|
||||
}
|
||||
throw new ArgumentException("Invalid track slug. " +
|
||||
"Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]");
|
||||
|
||||
EpisodeSlug = Episode.GetSlug(match.Groups["show"].Value,
|
||||
match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : null,
|
||||
match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : null);
|
||||
Language = match.Groups["language"].Value;
|
||||
EpisodeSlug = match.Groups["ep"].Value;
|
||||
Language = match.Groups["lang"].Value;
|
||||
TrackIndex = int.Parse(match.Groups["index"].Value);
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <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>()
|
||||
.Property(x => x.Slug)
|
||||
.ValueGeneratedOnAddOrUpdate();
|
||||
|
||||
// modelBuilder.Ignore<LibraryItem>();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Check if the exception is a duplicated exception.
|
||||
/// </summary>
|
||||
|
@ -13,7 +13,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.slug := CONCAT(
|
||||
NEW.slug := CONCAT(
|
||||
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
||||
'-s',
|
||||
NEW.season_number
|
||||
@ -38,10 +38,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
NEW.slug := CONCAT(
|
||||
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
||||
CASE
|
||||
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)
|
||||
ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number)
|
||||
END
|
||||
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)
|
||||
ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number)
|
||||
END
|
||||
);
|
||||
RETURN NEW;
|
||||
END
|
||||
@ -63,20 +63,80 @@ namespace Kyoo.Postgresql.Migrations
|
||||
BEGIN
|
||||
UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id;
|
||||
UPDATE episodes SET slug = CASE
|
||||
WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug
|
||||
WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number)
|
||||
ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number)
|
||||
END
|
||||
WHERE show_id = NEW.id;
|
||||
WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug
|
||||
WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number)
|
||||
ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number)
|
||||
END WHERE show_id = NEW.id;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;");
|
||||
|
||||
// language=PostgreSQL
|
||||
migrationBuilder.Sql(@"
|
||||
CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows
|
||||
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
|
||||
migrationBuilder.Sql(@"
|
||||
@ -112,6 +172,14 @@ namespace Kyoo.Postgresql.Migrations
|
||||
// language=PostgreSQL
|
||||
migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;");
|
||||
// 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;");
|
||||
}
|
||||
}
|
||||
|
@ -25,13 +25,13 @@ namespace Kyoo.SqLite.Migrations
|
||||
migrationBuilder.Sql(@"
|
||||
CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE Episodes
|
||||
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||
CASE
|
||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||
END
|
||||
UPDATE Episodes
|
||||
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||
CASE
|
||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||
END
|
||||
WHERE ID == new.ID;
|
||||
END");
|
||||
// language=SQLite
|
||||
@ -39,29 +39,114 @@ namespace Kyoo.SqLite.Migrations
|
||||
CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID
|
||||
ON Episodes FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE Episodes
|
||||
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||
CASE
|
||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||
END
|
||||
UPDATE Episodes
|
||||
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||
CASE
|
||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||
END
|
||||
WHERE ID == new.ID;
|
||||
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
|
||||
migrationBuilder.Sql(@"
|
||||
CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID;
|
||||
UPDATE Episodes
|
||||
SET Slug = new.Slug ||
|
||||
CASE
|
||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||
END
|
||||
UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID;
|
||||
UPDATE Episodes
|
||||
SET Slug = new.Slug ||
|
||||
CASE
|
||||
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||
END
|
||||
WHERE ShowID = new.ID;
|
||||
END;");
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Xunit;
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Xunit;
|
||||
@ -34,5 +35,17 @@ namespace Kyoo.Tests.Library
|
||||
{
|
||||
_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)
|
||||
{
|
||||
await Database.Entry(resource).Collection(x => x.Tracks).LoadAsync();
|
||||
await _tracks.DeleteAll(x => x.EpisodeID == resource.ID);
|
||||
resource.Tracks = changed.Tracks;
|
||||
await ValidateTracks(resource);
|
||||
}
|
||||
@ -148,14 +148,10 @@ namespace Kyoo.Controllers
|
||||
/// <returns>The <see cref="resource"/> parameter is returned.</returns>
|
||||
private async Task<Episode> ValidateTracks(Episode resource)
|
||||
{
|
||||
resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.MapAsync((x, i) =>
|
||||
resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.SelectAsync(x =>
|
||||
{
|
||||
x.Episode = resource;
|
||||
// TODO use a trigger for the next line.
|
||||
x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language
|
||||
&& x.IsForced == y.IsForced
|
||||
&& x.Codec == y.Codec
|
||||
&& x.Type == y.Type);
|
||||
x.EpisodeSlug = resource.Slug;
|
||||
return _tracks.Create(x);
|
||||
}).ToListAsync());
|
||||
return resource;
|
||||
|
@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
@ -34,56 +31,6 @@ namespace Kyoo.Controllers
|
||||
_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 />
|
||||
public override Task<ICollection<Track>> Search(string query)
|
||||
{
|
||||
@ -93,6 +40,9 @@ namespace Kyoo.Controllers
|
||||
/// <inheritdoc />
|
||||
public override async Task<Track> Create(Track obj)
|
||||
{
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
|
||||
if (obj.EpisodeID <= 0)
|
||||
{
|
||||
obj.EpisodeID = obj.Episode?.ID ?? 0;
|
||||
@ -102,14 +52,7 @@ namespace Kyoo.Controllers
|
||||
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
|
||||
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;
|
||||
});
|
||||
await _database.SaveChangesAsync();
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Options;
|
||||
using Kyoo.Postgresql;
|
||||
using Kyoo.SqLite;
|
||||
using Kyoo.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -27,19 +26,9 @@ namespace Kyoo.Api
|
||||
[Permission(nameof(SubtitleApi), Kind.Read)]
|
||||
public async Task<IActionResult> GetSubtitle(string slug, string extension)
|
||||
{
|
||||
Track subtitle;
|
||||
try
|
||||
{
|
||||
subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new {error = ex.Message});
|
||||
}
|
||||
|
||||
if (subtitle is not {Type: StreamType.Subtitle})
|
||||
Track subtitle = await _libraryManager.GetOrDefault<Track>(Track.EditSlug(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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user