Handling tracks slugs

This commit is contained in:
Zoe Roux 2021-07-13 15:18:25 +02:00
parent 6bd7b47fd9
commit dc42ed031f
15 changed files with 258 additions and 249 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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;
}
} }
} }

View File

@ -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>

View File

@ -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;");
} }
} }

View File

@ -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;");

View File

@ -1,4 +1,3 @@
using System.Linq;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
using Xunit; using Xunit;

View File

@ -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);
}
} }
} }

View File

@ -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;

View File

@ -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;
} }

View File

@ -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;

View File

@ -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);