From b0cc8d4194188644fabb7b4ef7c6eb1474c353a1 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Sat, 12 Oct 2024 00:26:03 +0200 Subject: [PATCH 1/7] fix: don't clean up VFS if it doesn't exist --- Shokofin/Resolvers/VirtualFileSystemService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Shokofin/Resolvers/VirtualFileSystemService.cs b/Shokofin/Resolvers/VirtualFileSystemService.cs index 7978461c..14168ed6 100644 --- a/Shokofin/Resolvers/VirtualFileSystemService.cs +++ b/Shokofin/Resolvers/VirtualFileSystemService.cs @@ -1001,6 +1001,12 @@ private List FindSubtitlesForPath(string sourcePath) private LinkGenerationResult CleanupStructure(string vfsPath, string directoryToClean, IReadOnlyList allKnownPaths, bool preview = false) { + if (!FileSystem.DirectoryExists(directoryToClean)) { + if (!preview) + Logger.LogDebug("Skipped cleaning up folder because it does not exist: {Path}", directoryToClean); + return new(); + } + if (!preview) Logger.LogDebug("Looking for files to remove in folder at {Path}", directoryToClean); var start = DateTime.Now; From 74418da87b7b364965382fb8628768793881abf1 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Sat, 12 Oct 2024 23:49:02 +0200 Subject: [PATCH 2/7] repo: update read-me [skip ci] --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 50dc50b5..88dc9788 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,15 @@ GitHub repository, or you can build it from source. Below is a version compatibility matrix for which version of Shokofin is compatible with what. -| Shokofin | Jellyfin | Shoko Server | -|------------|----------|---------------| -| `0.x.x` | `10.7` | `4.0.0-4.1.2` | -| `1.x.x` | `10.7` | `4.1.0-4.1.2` | -| `2.x.x` | `10.8` | `4.1.2` | -| `3.x.x` | `10.8` | `4.2.0` | -| `4.x.x` | `10.9` | `4.2.2` | -| `dev` | `10.9` | `dev` | +| Shokofin | Jellyfin | Shoko Server | +|-------------------|----------|---------------| +| `0.x.x` | `10.7` | `4.0.0` — `4.1.2` | +| `1.x.x` | `10.7` | `4.1.0` — `4.1.2` | +| `2.x.x` | `10.8` | `4.1.2` | +| `3.x.x` | `10.8` | `4.2.0` | +| `4.0.0` — `4.1.1` | `10.9` | `4.2.2` | +| `4.2.0` — `4.x.x` | `10.9` | `4.2.2` — `5.0.0` | +| `dev` | `10.9` | `dev` | ### Official Repository From c9405045bcfa9ebf11aeaadfca84ad1bce972591 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Mon, 14 Oct 2024 07:41:04 +0200 Subject: [PATCH 3/7] fix: auto remove old media folder mappings - Automagically remove old media folder mappings. --- .../Configuration/MediaFolderConfigurationService.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Shokofin/Configuration/MediaFolderConfigurationService.cs b/Shokofin/Configuration/MediaFolderConfigurationService.cs index 1f8a41d6..bf6a7d46 100644 --- a/Shokofin/Configuration/MediaFolderConfigurationService.cs +++ b/Shokofin/Configuration/MediaFolderConfigurationService.cs @@ -312,7 +312,7 @@ private async Task GenerateAllConfigurations(List allVirtualF foreach (var mediaFolderPath in virtualFolder.Locations) { if (LibraryManager.FindByPath(mediaFolderPath, true) is not Folder secondFolder) { - Logger.LogTrace("Unable to find database entry for {Path}", mediaFolderPath); + Logger.LogTrace("Unable to find database entry for {Path} (Library={LibraryId})", mediaFolderPath, libraryId); continue; } @@ -353,6 +353,15 @@ private async Task GenerateAllConfigurations(List allVirtualF edits.remove.Add(location); } } + + var mediaFoldersToRemove = config.MediaFolders + .Where(c => !filteredVirtualFolders.Any(v => Guid.Parse(v.ItemId) == c.LibraryId)) + .ToList(); + Logger.LogDebug("Found {Count} out of {TotalCount} media folders to remove.", mediaFoldersToRemove.Count, config.MediaFolders.Count); + foreach (var mediaFolder in mediaFoldersToRemove) { + Logger.LogTrace("Removing config for media folder at path {Path} (Library={LibraryId})", mediaFolder.MediaFolderPath, mediaFolder.LibraryId); + config.MediaFolders.Remove(mediaFolder); + } } private async Task CreateConfigurationForPath(Guid libraryId, Folder mediaFolder, MediaFolderConfiguration? libraryConfig) From b1280fb5836fc2b4fb418cce01fe5f8b093986a2 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 15 Oct 2024 04:09:59 +0200 Subject: [PATCH 4/7] fix: fix part episodes - Move the .ptX in partial episodes so Jellyfin properly recognises the parts. --- Shokofin/Resolvers/VirtualFileSystemService.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Shokofin/Resolvers/VirtualFileSystemService.cs b/Shokofin/Resolvers/VirtualFileSystemService.cs index 14168ed6..98739dc3 100644 --- a/Shokofin/Resolvers/VirtualFileSystemService.cs +++ b/Shokofin/Resolvers/VirtualFileSystemService.cs @@ -793,9 +793,7 @@ await Task.WhenAll(allFiles.Select(async (tuple) => { ExtraType.Sample => ["samples"], _ => ["extras"], }; - var 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}" - : ""; + var filePartSuffix = ""; if (collectionType is CollectionType.movies || (collectionType is null && isMovieSeason)) { if (extrasFolders != null) { foreach (var extrasFolder in extrasFolders) @@ -823,7 +821,10 @@ 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}"; + 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}" + : ""; } } @@ -840,7 +841,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}]{Path.GetExtension(sourceLocation)}"; + var fileName = $"{episodeName} {(extraDetails.Count is > 0 ? $"[{extraDetails.Select(a => a.ReplaceInvalidPathCharacters()).Join("] [")}] " : "")}[{ShokoSeriesId.Name}={seriesId}] [{ShokoFileId.Name}={fileId}]{filePartSuffix}{Path.GetExtension(sourceLocation)}"; var symbolicLinks = folders .Select(folderPath => Path.Join(folderPath, fileName)) .ToArray(); From 83c4745785903af21e0ddb4e31b31d8eddfccd3d Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 15 Oct 2024 06:04:22 +0200 Subject: [PATCH 5/7] 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 From be157764012b7bd06a0c6d0bc424e6fb5d23cffb Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 15 Oct 2024 06:05:57 +0200 Subject: [PATCH 6/7] fix: ensure all media folder configurations are available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …before cleaning up the VFS roots, since we rely on the metadata being available to fix them properly. --- .../MediaFolderConfigurationService.cs | 10 ++++++++-- Shokofin/Events/EventDispatchService.cs | 2 +- Shokofin/Tasks/CleanupVirtualRootTask.cs | 18 +++++++++++------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Shokofin/Configuration/MediaFolderConfigurationService.cs b/Shokofin/Configuration/MediaFolderConfigurationService.cs index bf6a7d46..7994a5d2 100644 --- a/Shokofin/Configuration/MediaFolderConfigurationService.cs +++ b/Shokofin/Configuration/MediaFolderConfigurationService.cs @@ -209,11 +209,17 @@ private async void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventAr #region Media Folder Mapping - public IReadOnlyList<(string vfsPath, string mainMediaFolderPath, CollectionType? collectionType, IReadOnlyList mediaList)> GetAvailableMediaFoldersForLibrariesForEvents(Func? filter = null) + public async Task mediaList)>> GetAvailableMediaFoldersForLibraries(Func? filter = null) { - LockObj.Wait(); + await LockObj.WaitAsync(); try { var virtualFolders = LibraryManager.GetVirtualFolders(); + if (ShouldGenerateAllConfigurations) + { + ShouldGenerateAllConfigurations = false; + await GenerateAllConfigurations(virtualFolders).ConfigureAwait(false); + } + var attachRoot = Plugin.Instance.Configuration.VFS_AttachRoot; return Plugin.Instance.Configuration.MediaFolders .Where(config => config.IsMapped && !config.IsVirtualRoot && (filter is null || filter(config)) && LibraryManager.GetItemById(config.MediaFolderId) is Folder) diff --git a/Shokofin/Events/EventDispatchService.cs b/Shokofin/Events/EventDispatchService.cs index d9199f60..e8c0cfe4 100644 --- a/Shokofin/Events/EventDispatchService.cs +++ b/Shokofin/Events/EventDispatchService.cs @@ -211,7 +211,7 @@ private async Task ProcessFileEvents(int fileId, List<(UpdateReason Reason, int var locationsToNotify = new List(); var mediaFoldersToNotify = new Dictionary(); var seriesIds = await GetSeriesIdsForFile(fileId, changes.Select(t => t.Event).LastOrDefault(e => e.HasCrossReferences)); - var libraries = ConfigurationService.GetAvailableMediaFoldersForLibrariesForEvents(c => c.IsFileEventsEnabled); + var libraries = await ConfigurationService.GetAvailableMediaFoldersForLibraries(c => c.IsFileEventsEnabled).ConfigureAwait(false); var (reason, importFolderId, relativePath, lastEvent) = changes.Last(); if (reason is not UpdateReason.Removed) { Logger.LogTrace("Processing file changed. (File={FileId})", fileId); diff --git a/Shokofin/Tasks/CleanupVirtualRootTask.cs b/Shokofin/Tasks/CleanupVirtualRootTask.cs index d3bbf4e9..0f083837 100644 --- a/Shokofin/Tasks/CleanupVirtualRootTask.cs +++ b/Shokofin/Tasks/CleanupVirtualRootTask.cs @@ -9,6 +9,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using Shokofin.Configuration; using Shokofin.Utils; namespace Shokofin.Tasks; @@ -16,7 +17,7 @@ namespace Shokofin.Tasks; /// /// Clean-up any old VFS roots leftover from an outdated install or failed removal of the roots. /// -public class CleanupVirtualRootTask(ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, LibraryScanWatcher scanWatcher) : IScheduledTask, IConfigurableScheduledTask +public class CleanupVirtualRootTask(ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, MediaFolderConfigurationService configurationService, LibraryScanWatcher scanWatcher) : IScheduledTask, IConfigurableScheduledTask { /// public string Name => "Clean-up Virtual File System Roots"; @@ -45,6 +46,8 @@ public class CleanupVirtualRootTask(ILogger logger, ILib private readonly IFileSystem FileSystem = fileSystem; + private readonly MediaFolderConfigurationService ConfigurationService = configurationService; + private readonly LibraryScanWatcher ScanWatcher = scanWatcher; public IEnumerable GetDefaultTriggers() @@ -54,11 +57,14 @@ public IEnumerable GetDefaultTriggers() }, ]; - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { if (ScanWatcher.IsScanRunning) - return Task.CompletedTask; + return; + var mediaFolders = (await ConfigurationService.GetAvailableMediaFoldersForLibraries().ConfigureAwait(false)) + .SelectMany(x => x.mediaList) + .ToList(); var start = DateTime.Now; var virtualRoots = Plugin.Instance.AllVirtualRoots .Except([Plugin.Instance.VirtualRoot]) @@ -73,7 +79,7 @@ public Task ExecuteAsync(IProgress progress, CancellationToken cancellat Logger.LogTrace("Removed VFS root {Path} in {TimeSpan}.", virtualRoot, perFolderDeltaTime); } - var libraryIds = Plugin.Instance.Configuration.MediaFolders.ToList() + var libraryIds = mediaFolders.ToList() .Select(config => config.LibraryId.ToString()) .Distinct() .ToList(); @@ -96,7 +102,7 @@ public Task ExecuteAsync(IProgress progress, CancellationToken cancellat start = DateTime.Now; var addedCount = 0; var fixedCount = 0; - var vfsPaths = Plugin.Instance.Configuration.MediaFolders + var vfsPaths = mediaFolders .DistinctBy(config => config.LibraryId) .Select(config => LibraryManager.GetItemById(config.LibraryId) as Folder) .Where(folder => folder is not null) @@ -123,7 +129,5 @@ public Task ExecuteAsync(IProgress progress, CancellationToken cancellat deltaTime = DateTime.Now - start; Logger.LogDebug("Added {AddedCount} missing and fixed {FixedCount} broken VFS roots in {TimeSpan}.", addedCount, fixedCount, deltaTime); } - - return Task.CompletedTask; } } From e40431462e7e05fc6e022154c54993ea45b8f81c Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Thu, 17 Oct 2024 22:02:56 +0200 Subject: [PATCH 7/7] fix: scan all known libraries if series is gone - Scan all known libraries managed by the plugin if we receive one or more file removed events and the series is gone by the time we ask the server for the series related to the file. Since we won't be able to later ask the server for any series related metadata to generate the VFS path to check, so our next best bet is to just ask the core to scan the whole library to let the VFS generation logic take care of the removal of the file(s). --- Shokofin/Events/EventDispatchService.cs | 33 ++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/Shokofin/Events/EventDispatchService.cs b/Shokofin/Events/EventDispatchService.cs index e8c0cfe4..91845b7b 100644 --- a/Shokofin/Events/EventDispatchService.cs +++ b/Shokofin/Events/EventDispatchService.cs @@ -281,6 +281,19 @@ private async Task ProcessFileEvents(int fileId, List<(UpdateReason Reason, int } // Something was removed, so assume the location is gone. else if (changes.FirstOrDefault(t => t.Reason is UpdateReason.Removed).Event is IFileEventArgs firstRemovedEvent) { + // If we don't know which series to remove, then add all of them to be scanned. + if (seriesIds.Count is 0) { + Logger.LogTrace("No series found for file. Adding all libraries. (File={FileId})", fileId); + foreach (var (vfsPath, mainMediaFolderPath, collectionType, mediaConfigs) in libraries) { + // Give the core logic _any_ file or folder placed directly in the media folder, so it will schedule the media folder to be refreshed. + var fileOrFolder = FileSystem.GetFileSystemEntryPaths(mainMediaFolderPath, false).FirstOrDefault(); + if (!string.IsNullOrEmpty(fileOrFolder)) + mediaFoldersToNotify.TryAdd(mainMediaFolderPath, (fileOrFolder, mainMediaFolderPath.GetFolderForPath())); + } + + goto aLabelToReduceNesting; + } + Logger.LogTrace("Processing file removed. (File={FileId})", fileId); relativePath = firstRemovedEvent.RelativePath; importFolderId = firstRemovedEvent.ImportFolderId; @@ -351,6 +364,7 @@ private async Task ProcessFileEvents(int fileId, List<(UpdateReason Reason, int } } + aLabelToReduceNesting:; if (LibraryScanWatcher.IsScanRunning) { Logger.LogDebug("Skipped notifying Jellyfin about {LocationCount} changes because a library scan is running. (File={FileId})", locationsToNotify.Count, fileId.ToString()); return; @@ -362,6 +376,7 @@ private async Task ProcessFileEvents(int fileId, List<(UpdateReason Reason, int LibraryMonitor.ReportFileSystemChanged(location); if (mediaFoldersToNotify.Count > 0) await Task.WhenAll(mediaFoldersToNotify.Values.Select(tuple => ReportMediaFolderChanged(tuple.mediaFolder, tuple.pathToReport))).ConfigureAwait(false); + Logger.LogDebug("Notified Jellyfin about {LocationCount} changes. (File={FileId})", locationsToNotify.Count + mediaFoldersToNotify.Count, fileId.ToString()); } catch (Exception ex) { Logger.LogError(ex, "Error processing {EventCount} file change events. (File={FileId})", changes.Count, fileId); @@ -397,15 +412,21 @@ private async Task> GetSeriesIdsForFile(int fileId, IFileEv var filteredSeriesIds = new HashSet(); foreach (var seriesId in seriesIds) { - var (primaryId, extraIds) = await ApiManager.GetSeriesIdsForSeason(seriesId); - var seriesPathSet = await ApiManager.GetPathSetForSeries(primaryId, extraIds); - if (seriesPathSet.Count > 0) { - filteredSeriesIds.Add(seriesId); + try { + var (primaryId, extraIds) = await ApiManager.GetSeriesIdsForSeason(seriesId); + var seriesPathSet = await ApiManager.GetPathSetForSeries(primaryId, extraIds); + if (seriesPathSet.Count > 0) { + filteredSeriesIds.Add(seriesId); + } + } + // If we fail to find the series data (most likely because it's already gone) then just abort early. We'll handle it elsewhere. + catch (ApiException ex) when (ex.StatusCode is System.Net.HttpStatusCode.NotFound) { + return new HashSet(); } } // Return all series if we only have this file for all of them, - // otherwise return only the series were we have other files that are + // otherwise return only the series where we have other files that are // not linked to other series. return filteredSeriesIds.Count is 0 ? seriesIds : filteredSeriesIds; } @@ -434,7 +455,7 @@ private async Task> GetSeriesIdsForFile(int fileId, IFileEv private void RemoveSymbolicLink(string filePath) { - // TODO: If this works better, the move it to an utility and also use it in the VFS if needed, or remove this comment if it's not needed. + // TODO: If this works better, then move it to an utility and also use it in the VFS if needed, or remove this comment if that's not needed. try { var fileExists = File.Exists(filePath); var fileInfo = new System.IO.FileInfo(filePath);