From 83c4745785903af21e0ddb4e31b31d8eddfccd3d Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 15 Oct 2024 06:04:22 +0200 Subject: [PATCH] fix: fix part episodes (take 2) - Fixed it so the paths match _exactly_ for the episode parts except for the part number, since Jellyfin's internal logic didn't like that the file ids were different between the parts. The fix was to add all file ids to the file name and select the correct id for the given part. --- Shokofin/API/Models/CrossReference.cs | 10 ++++++++++ Shokofin/API/ShokoAPIClient.cs | 5 +++++ .../Resolvers/VirtualFileSystemService.cs | 13 ++++++++---- Shokofin/StringExtensions.cs | 20 +++++++++++++++++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Shokofin/API/Models/CrossReference.cs b/Shokofin/API/Models/CrossReference.cs index 244bea8b..8632e360 100644 --- a/Shokofin/API/Models/CrossReference.cs +++ b/Shokofin/API/Models/CrossReference.cs @@ -35,6 +35,16 @@ public class EpisodeCrossReferenceIDs public int? ReleaseGroup { get; set; } + /// + /// ED2K hash. + /// + public string ED2K { get; set; } = string.Empty; + + /// + /// File size. + /// + public long FileSize { get; set; } + /// /// Percentage file is matched to the episode. /// diff --git a/Shokofin/API/ShokoAPIClient.cs b/Shokofin/API/ShokoAPIClient.cs index 0d556b06..138c993d 100644 --- a/Shokofin/API/ShokoAPIClient.cs +++ b/Shokofin/API/ShokoAPIClient.cs @@ -276,6 +276,11 @@ public Task GetFile(string id) return Get($"/api/v3/File/{id}?include=XRefs&includeDataFrom=AniDB"); } + public Task GetFileByEd2kAndFileSize(string ed2k, long fileSize) + { + return Get($"/api/v3/File/Hash/ED2K?hash={Uri.EscapeDataString(ed2k)}&size={fileSize}&includeDataFrom=AniDB"); + } + public Task> GetFileByPath(string path) { return Get>($"/api/v3/File/PathEndsWith?path={Uri.EscapeDataString(path)}&includeDataFrom=AniDB&limit=1"); diff --git a/Shokofin/Resolvers/VirtualFileSystemService.cs b/Shokofin/Resolvers/VirtualFileSystemService.cs index 98739dc3..3f5ee189 100644 --- a/Shokofin/Resolvers/VirtualFileSystemService.cs +++ b/Shokofin/Resolvers/VirtualFileSystemService.cs @@ -793,6 +793,7 @@ await Task.WhenAll(allFiles.Select(async (tuple) => { ExtraType.Sample => ["samples"], _ => ["extras"], }; + var fileIdList = fileId; var filePartSuffix = ""; if (collectionType is CollectionType.movies || (collectionType is null && isMovieSeason)) { if (extrasFolders != null) { @@ -822,9 +823,13 @@ await Task.WhenAll(allFiles.Select(async (tuple) => { else { folders.Add(Path.Join(vfsPath, showFolder, seasonFolder)); episodeName = $"{showName} S{(isSpecial ? 0 : seasonNumber).ToString().PadLeft(2, '0')}E{episodeNumber.ToString().PadLeft(show.EpisodePadding, '0')}"; - filePartSuffix = (episodeXref.Percentage?.Group ?? 1) is not 1 - ? $".pt{episode.Shoko.CrossReferences.Where(xref => xref.ReleaseGroup == episodeXref.ReleaseGroup && xref.Percentage!.Group == episodeXref.Percentage!.Group).ToList().FindIndex(xref => xref.Percentage!.Start == episodeXref.Percentage!.Start && xref.Percentage!.End == episodeXref.Percentage!.End) + 1}" - : ""; + if ((episodeXref.Percentage?.Group ?? 1) is not 1) { + var list = episode.Shoko.CrossReferences.Where(xref => xref.ReleaseGroup == episodeXref.ReleaseGroup && xref.Percentage!.Group == episodeXref.Percentage!.Group).ToList(); + var files = await Task.WhenAll(list.Select(xref => ApiClient.GetFileByEd2kAndFileSize(xref.ED2K, xref.FileSize))); + var index = list.FindIndex(xref => xref.Percentage!.Start == episodeXref.Percentage!.Start && xref.Percentage!.End == episodeXref.Percentage!.End); + filePartSuffix = $".pt{index + 1}"; + fileIdList = files.Select(f => f.Id.ToString()).Join(","); + } } } @@ -841,7 +846,7 @@ file.Shoko.AniDBData is not null ); if (config.VFS_AddResolution && !string.IsNullOrEmpty(file.Shoko.Resolution)) extraDetails.Add(file.Shoko.Resolution); - var fileName = $"{episodeName} {(extraDetails.Count is > 0 ? $"[{extraDetails.Select(a => a.ReplaceInvalidPathCharacters()).Join("] [")}] " : "")}[{ShokoSeriesId.Name}={seriesId}] [{ShokoFileId.Name}={fileId}]{filePartSuffix}{Path.GetExtension(sourceLocation)}"; + var fileName = $"{episodeName} {(extraDetails.Count is > 0 ? $"[{extraDetails.Select(a => a.ReplaceInvalidPathCharacters()).Join("] [")}] " : "")}[{ShokoSeriesId.Name}={seriesId}] [{ShokoFileId.Name}={fileIdList}]{filePartSuffix}{Path.GetExtension(sourceLocation)}"; var symbolicLinks = folders .Select(folderPath => Path.Join(folderPath, fileName)) .ToArray(); diff --git a/Shokofin/StringExtensions.cs b/Shokofin/StringExtensions.cs index 5494e58f..1f21d7ee 100644 --- a/Shokofin/StringExtensions.cs +++ b/Shokofin/StringExtensions.cs @@ -4,10 +4,11 @@ using System.Linq; using System.Text.RegularExpressions; using MediaBrowser.Common.Providers; +using Shokofin.ExternalIds; namespace Shokofin; -public static class StringExtensions +public static partial class StringExtensions { public static string Replace(this string input, Regex regex, string replacement, int count, int startAt) => regex.Replace(input, replacement, count, startAt); @@ -152,6 +153,21 @@ public static string ReplaceInvalidPathCharacters(this string path) return null; } + [GeneratedRegex(@"\.pt(?\d+)(?:\.[a-z0-9]+)?$", RegexOptions.IgnoreCase)] + private static partial Regex GetPartRegex(); + public static bool TryGetAttributeValue(this string text, string attribute, [NotNullWhen(true)] out string? value) - => !string.IsNullOrEmpty(value = GetAttributeValue(text, attribute)); + { + value = GetAttributeValue(text, attribute); + + // Select the correct id for the part number in the stringified list of file ids. + if (!string.IsNullOrEmpty(value) && attribute == ShokoFileId.Name && GetPartRegex().Match(text) is { Success: true } regexResult) { + var partNumber = int.Parse(regexResult.Groups["partNumber"].Value); + var index = partNumber - 1; + value = value.Split(',')[index]; + } + + return !string.IsNullOrEmpty(value); + } + } \ No newline at end of file