Adding a TrackIndex for tracks, support duplicated values & slugs for tracks that are not subtitles

This commit is contained in:
Zoe Roux 2021-03-18 00:16:14 +01:00
parent aac1975a75
commit 2138f0909b
9 changed files with 59 additions and 23 deletions

View File

@ -103,19 +103,13 @@ namespace Kyoo.Models
StreamType.Font => "font.", StreamType.Font => "font.",
_ => "" _ => ""
}; };
string slug = $"{Episode.Slug}.{type}{Language}{(TrackIndex != 0 ? TrackIndex : "")}"; string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
if (IsForced) string codec = Codec switch
slug += "-forced";
switch (Codec)
{ {
case "ass": "subrip" => ".srt",
slug += ".ass"; {} x => $".{x}"
break; };
case "subrip": return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}";
slug += ".srt";
break;
}
return slug;
} }
} }

View File

@ -188,7 +188,7 @@ namespace Kyoo.Controllers
&& x.IsForced == y.IsForced && x.IsForced == y.IsForced
&& x.Codec == y.Codec && x.Codec == y.Codec
&& x.Type == y.Type); && x.Type == y.Type);
return _tracks.CreateIfNotExists(x, true); return _tracks.Create(x);
}).ToListAsync(); }).ToListAsync();
return resource; return resource;
} }

View File

@ -4,6 +4,7 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions; 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
@ -12,7 +13,7 @@ namespace Kyoo.Controllers
{ {
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
protected override Expression<Func<Track, object>> DefaultSort => x => x.ID; protected override Expression<Func<Track, object>> DefaultSort => x => x.TrackIndex;
public TrackRepository(DatabaseContext database) : base(database) public TrackRepository(DatabaseContext database) : base(database)
@ -44,7 +45,7 @@ namespace Kyoo.Controllers
public Task<Track> Get(string slug, StreamType type) public Task<Track> Get(string slug, StreamType type)
{ {
Match match = Regex.Match(slug, Match match = Regex.Match(slug,
@"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?"); @"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success) if (!match.Success)
{ {
@ -61,6 +62,8 @@ namespace Kyoo.Controllers
int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1; int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1;
string language = match.Groups["language"].Value; string language = match.Groups["language"].Value;
bool forced = match.Groups["forced"].Success; bool forced = match.Groups["forced"].Success;
if (match.Groups["type"].Success)
type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
if (type == StreamType.Unknown) if (type == StreamType.Unknown)
{ {
@ -94,7 +97,13 @@ namespace Kyoo.Controllers
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated track (slug {obj.Slug} already exists)."); 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

@ -185,6 +185,9 @@ namespace Kyoo
modelBuilder.Entity<Episode>() modelBuilder.Entity<Episode>()
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
.IsUnique(); .IsUnique();
modelBuilder.Entity<Track>()
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique();
} }
public T GetTemporaryObject<T>(T model) public T GetTemporaryObject<T>(T model)
@ -301,6 +304,33 @@ namespace Kyoo
} }
} }
public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new())
{
return SaveOrRetry(obj, onFail, 0, cancellationToken);
}
public 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;
}
}
private static bool IsDuplicateException(Exception ex) private static bool IsDuplicateException(Exception ex)
{ {
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};

View File

@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal namespace Kyoo.Models.DatabaseMigrations.Internal
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(DatabaseContext))]
[Migration("20210317180448_Initial")] [Migration("20210317220956_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -499,7 +499,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("EpisodeID"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique();
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });

View File

@ -543,9 +543,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Tracks_EpisodeID", name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced",
table: "Tracks", table: "Tracks",
column: "EpisodeID"); columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" },
unique: true);
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)

View File

@ -497,7 +497,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("EpisodeID"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique();
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });

View File

@ -196,7 +196,7 @@ namespace Kyoo
ctx.Response.Headers.Remove("X-Powered-By"); ctx.Response.Headers.Remove("X-Powered-By");
ctx.Response.Headers.Remove("Server"); ctx.Response.Headers.Remove("Server");
ctx.Response.Headers.Add("Feature-Policy", "autoplay 'self'; fullscreen"); ctx.Response.Headers.Add("Feature-Policy", "autoplay 'self'; fullscreen");
ctx.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"); ctx.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'");
ctx.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); ctx.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
ctx.Response.Headers.Add("Referrer-Policy", "no-referrer"); ctx.Response.Headers.Add("Referrer-Policy", "no-referrer");
ctx.Response.Headers.Add("Access-Control-Allow-Origin", "null"); ctx.Response.Headers.Add("Access-Control-Allow-Origin", "null");

View File

@ -35,7 +35,7 @@ namespace Kyoo.Api
return BadRequest(new {error = ex.Message}); return BadRequest(new {error = ex.Message});
} }
if (subtitle == null) if (subtitle == null || subtitle.Type != StreamType.Subtitle)
return NotFound(); return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt") if (subtitle.Codec == "subrip" && extension == "vtt")