From 2138f0909be524d7844bc4106f228278f81af81c Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 18 Mar 2021 00:16:14 +0100 Subject: [PATCH] Adding a TrackIndex for tracks, support duplicated values & slugs for tracks that are not subtitles --- Kyoo.Common/Models/Resources/Track.cs | 18 ++++------- .../Repositories/EpisodeRepository.cs | 2 +- .../Repositories/TrackRepository.cs | 15 ++++++++-- Kyoo/Models/DatabaseContext.cs | 30 +++++++++++++++++++ ....cs => 20210317220956_Initial.Designer.cs} | 5 ++-- ...8_Initial.cs => 20210317220956_Initial.cs} | 5 ++-- .../Internal/DatabaseContextModelSnapshot.cs | 3 +- Kyoo/Startup.cs | 2 +- Kyoo/Views/API/SubtitleApi.cs | 2 +- 9 files changed, 59 insertions(+), 23 deletions(-) rename Kyoo/Models/DatabaseMigrations/Internal/{20210317180448_Initial.Designer.cs => 20210317220956_Initial.Designer.cs} (99%) rename Kyoo/Models/DatabaseMigrations/Internal/{20210317180448_Initial.cs => 20210317220956_Initial.cs} (99%) diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index ec4d2919..544dc40e 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -103,19 +103,13 @@ namespace Kyoo.Models StreamType.Font => "font.", _ => "" }; - string slug = $"{Episode.Slug}.{type}{Language}{(TrackIndex != 0 ? TrackIndex : "")}"; - if (IsForced) - slug += "-forced"; - switch (Codec) + string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; + string codec = Codec switch { - case "ass": - slug += ".ass"; - break; - case "subrip": - slug += ".srt"; - break; - } - return slug; + "subrip" => ".srt", + {} x => $".{x}" + }; + return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; } } diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 34c689dc..77901701 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -188,7 +188,7 @@ namespace Kyoo.Controllers && x.IsForced == y.IsForced && x.Codec == y.Codec && x.Type == y.Type); - return _tracks.CreateIfNotExists(x, true); + return _tracks.Create(x); }).ToListAsync(); return resource; } diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index b4c5b00e..d4e04376 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -4,6 +4,7 @@ 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 @@ -12,7 +13,7 @@ namespace Kyoo.Controllers { private bool _disposed; private readonly DatabaseContext _database; - protected override Expression> DefaultSort => x => x.ID; + protected override Expression> DefaultSort => x => x.TrackIndex; public TrackRepository(DatabaseContext database) : base(database) @@ -44,7 +45,7 @@ namespace Kyoo.Controllers public Task Get(string slug, StreamType type) { Match match = Regex.Match(slug, - @"(?.*)-s(?\d+)e(?\d+)\.(?.{0,3})(?-forced)?(\..*)?"); + @"(?.*)-s(?\d+)e(?\d+)(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..*)?"); if (!match.Success) { @@ -61,6 +62,8 @@ namespace Kyoo.Controllers int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1; string language = match.Groups["language"].Value; bool forced = match.Groups["forced"].Success; + if (match.Groups["type"].Success) + type = Enum.Parse(match.Groups["type"].Value, true); if (type == StreamType.Unknown) { @@ -94,7 +97,13 @@ namespace Kyoo.Controllers await base.Create(obj); _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; } diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index e03f6677..25271455 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -185,6 +185,9 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) + .IsUnique(); } public T GetTemporaryObject(T model) @@ -301,6 +304,33 @@ namespace Kyoo } } + public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new()) + { + return SaveOrRetry(obj, onFail, 0, cancellationToken); + } + + public async Task SaveOrRetry(T obj, + Func 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) { return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.Designer.cs similarity index 99% rename from Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.Designer.cs index d62ff33f..b713dc6b 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.Designer.cs @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Models.DatabaseMigrations.Internal { [DbContext(typeof(DatabaseContext))] - [Migration("20210317180448_Initial")] + [Migration("20210317220956_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -499,7 +499,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasKey("ID"); - b.HasIndex("EpisodeID"); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); b.ToTable("Tracks"); }); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.cs similarity index 99% rename from Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.cs index 0593207e..b464f251 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210317180448_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.cs @@ -543,9 +543,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal unique: true); migrationBuilder.CreateIndex( - name: "IX_Tracks_EpisodeID", + name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced", table: "Tracks", - column: "EpisodeID"); + columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, + unique: true); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs index 9889f943..f3b66bd3 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs @@ -497,7 +497,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasKey("ID"); - b.HasIndex("EpisodeID"); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); b.ToTable("Tracks"); }); diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 03b63cdf..5135cd06 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -196,7 +196,7 @@ namespace Kyoo ctx.Response.Headers.Remove("X-Powered-By"); ctx.Response.Headers.Remove("Server"); 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("Referrer-Policy", "no-referrer"); ctx.Response.Headers.Add("Access-Control-Allow-Origin", "null"); diff --git a/Kyoo/Views/API/SubtitleApi.cs b/Kyoo/Views/API/SubtitleApi.cs index 132b3b19..8cdc124b 100644 --- a/Kyoo/Views/API/SubtitleApi.cs +++ b/Kyoo/Views/API/SubtitleApi.cs @@ -35,7 +35,7 @@ namespace Kyoo.Api return BadRequest(new {error = ex.Message}); } - if (subtitle == null) + if (subtitle == null || subtitle.Type != StreamType.Subtitle) return NotFound(); if (subtitle.Codec == "subrip" && extension == "vtt")