diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 5efa53e31f..b4d77bc4c6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.8", + "version": "9.0.9", "commands": [ "dotnet-ef" ] diff --git a/.editorconfig b/.editorconfig index ab5d3d9dd1..313b02563d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -294,6 +294,9 @@ dotnet_diagnostic.CA1854.severity = error # error on CA1860: Avoid using 'Enumerable.Any()' extension method dotnet_diagnostic.CA1860.severity = error +# error on CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = error + # error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons dotnet_diagnostic.CA1862.severity = error diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 4f0c1007f1..89d59e4c4a 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 82f9dc3c8e..f2cf967e93 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@c4c5175a441c6603ec614f5084386dabe0e2295b # v5.4.12 + uses: danielpalme/ReportGenerator-GitHub-Action@1978db745da4a573ca4baa2d0f67175df51a148c # v5.4.16 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 66ef2a07ed..0a4114478f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -31,6 +31,7 @@ - [DaveChild](https://github.com/DaveChild) - [DavidFair](https://github.com/DavidFair) - [Delgan](https://github.com/Delgan) + - [Derpipose](https://github.com/Derpipose) - [dcrdev](https://github.com/dcrdev) - [dhartung](https://github.com/dhartung) - [dinki](https://github.com/dinki) @@ -140,6 +141,7 @@ - [ThibaultNocchi](https://github.com/ThibaultNocchi) - [thornbill](https://github.com/thornbill) - [ThreeFive-O](https://github.com/ThreeFive-O) + - [tjwalkr3](https://github.com/tjwalkr3) - [TrisMcC](https://github.com/TrisMcC) - [trumblejoe](https://github.com/trumblejoe) - [TtheCreator](https://github.com/TtheCreator) diff --git a/Directory.Build.props b/Directory.Build.props index 31ae8bfbe4..8400f4c5e7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,4 +19,9 @@ + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 7547919b88..35f8ed4cdc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,40 +26,43 @@ - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -70,27 +73,27 @@ - + - + - - - + + + - - + + - + \ No newline at end of file diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 1c518f0cca..f61ca7e129 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -21,8 +21,8 @@ namespace Emby.Naming.Common /// public NamingOptions() { - VideoFileExtensions = new[] - { + VideoFileExtensions = + [ ".001", ".3g2", ".3gp", @@ -77,10 +77,10 @@ namespace Emby.Naming.Common ".wmv", ".wtv", ".xvid" - }; + ]; - VideoFlagDelimiters = new[] - { + VideoFlagDelimiters = + [ '(', ')', '-', @@ -88,15 +88,15 @@ namespace Emby.Naming.Common '_', '[', ']' - }; + ]; - StubFileExtensions = new[] - { + StubFileExtensions = + [ ".disc" - }; + ]; - StubTypes = new[] - { + StubTypes = + [ new StubTypeRule( stubType: "dvd", token: "dvd"), @@ -136,32 +136,32 @@ namespace Emby.Naming.Common new StubTypeRule( stubType: "tv", token: "DSR") - }; + ]; - VideoFileStackingRules = new[] - { + VideoFileStackingRules = + [ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[0-9]+)[\)\]]?(?:\.[^.]+)?$", true), new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[a-d])[\)\]]?(?:\.[^.]+)?$", false) - }; + ]; - CleanDateTimes = new[] - { + CleanDateTimes = + [ @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" - }; + ]; - CleanStrings = new[] - { + CleanStrings = + [ @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"^(?.+?)(\[.*\])", @"^\s*(?.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?.+)", @"^\s*(?.+?)\s+-\s+[0-9]+\s*$", @"^\s*(?.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$" - }; + ]; - SubtitleFileExtensions = new[] - { + SubtitleFileExtensions = + [ ".ass", ".mks", ".sami", @@ -171,17 +171,17 @@ namespace Emby.Naming.Common ".sub", ".sup", ".vtt", - }; + ]; - LyricFileExtensions = new[] - { + LyricFileExtensions = + [ ".lrc", ".elrc", ".txt" - }; + ]; - AlbumStackingPrefixes = new[] - { + AlbumStackingPrefixes = + [ "cd", "digital media", "disc", @@ -190,10 +190,10 @@ namespace Emby.Naming.Common "volume", "part", "act" - }; + ]; - ArtistSubfolders = new[] - { + ArtistSubfolders = + [ "albums", "broadcasts", "bootlegs", @@ -208,10 +208,10 @@ namespace Emby.Naming.Common "soundtracks", "spokenwords", "streets" - }; + ]; - AudioFileExtensions = new[] - { + AudioFileExtensions = + [ ".669", ".3gp", ".aa", @@ -241,6 +241,7 @@ namespace Emby.Naming.Common ".dts", ".dvf", ".eac3", + ".ec3", ".far", ".flac", ".gdm", @@ -291,33 +292,33 @@ namespace Emby.Naming.Common ".xm", ".xsp", ".ymf" - }; + ]; - MediaFlagDelimiters = new[] - { + MediaFlagDelimiters = + [ '.' - }; + ]; - MediaForcedFlags = new[] - { + MediaForcedFlags = + [ "foreign", "forced" - }; + ]; - MediaDefaultFlags = new[] - { + MediaDefaultFlags = + [ "default" - }; + ]; - MediaHearingImpairedFlags = new[] - { + MediaHearingImpairedFlags = + [ "cc", "hi", "sdh" - }; + ]; - EpisodeExpressions = new[] - { + EpisodeExpressions = + [ // *** Begin Kodi Standard Naming // new EpisodeExpression(@".*(\\|\/)(?((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?[0-9]+)[][ ._-]*[Ee](?[0-9]+)([^\\/]*)$") @@ -330,23 +331,23 @@ namespace Emby.Naming.Common new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), new EpisodeExpression("(?[0-9]{4})[._ -](?[0-9]{2})[._ -](?[0-9]{2})", true) { - DateTimeFormats = new[] - { + DateTimeFormats = + [ "yyyy.MM.dd", "yyyy-MM-dd", "yyyy_MM_dd", "yyyy MM dd" - } + ] }, new EpisodeExpression("(?[0-9]{2})[._ -](?[0-9]{2})[._ -](?[0-9]{4})", true) { - DateTimeFormats = new[] - { + DateTimeFormats = + [ "dd.MM.yyyy", "dd-MM-yyyy", "dd_MM_yyyy", "dd MM yyyy" - } + ] }, // This isn't a Kodi naming rule, but the expression below causes false episode numbers for @@ -478,10 +479,10 @@ namespace Emby.Naming.Common { IsNamed = true }, - }; + ]; - VideoExtraRules = new[] - { + VideoExtraRules = + [ new ExtraRule( ExtraType.Trailer, ExtraRuleType.DirectoryName, @@ -691,14 +692,14 @@ namespace Emby.Naming.Common ExtraRuleType.Suffix, "-other", MediaType.Video) - }; + ]; AllExtrasTypesFolderNames = VideoExtraRules .Where(i => i.RuleType == ExtraRuleType.DirectoryName) .ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase); - Format3DRules = new[] - { + Format3DRules = + [ // Kodi rules: new Format3DRule( precedingToken: "3d", @@ -725,10 +726,10 @@ namespace Emby.Naming.Common new Format3DRule("tab"), new Format3DRule("sbs3d"), new Format3DRule("mvc") - }; + ]; - AudioBookPartsExpressions = new[] - { + AudioBookPartsExpressions = + [ // Detect specified chapters, like CH 01 @"ch(?:apter)?[\s_-]?(?[0-9]+)", // Detect specified parts, like Part 02 @@ -741,14 +742,14 @@ namespace Emby.Naming.Common "(?[0-9]+)_(?[0-9]+)", // Some audiobooks are ripped from cd's, and will be named by disk number. @"dis(?:c|k)[\s_-]?(?[0-9]+)" - }; + ]; - AudioBookNamesExpressions = new[] - { + AudioBookNamesExpressions = + [ // Detect year usually in brackets after name Batman (2020) @"^(?.+?)\s*\(\s*(?[0-9]{4})\s*\)\s*$", @"^\s*(?[^ ].*?)\s*$" - }; + ]; MultipleEpisodeExpressions = new[] { @@ -888,12 +889,12 @@ namespace Emby.Naming.Common /// /// Gets list of clean datetime regular expressions. /// - public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty(); + public Regex[] CleanDateTimeRegexes { get; private set; } = []; /// /// Gets list of clean string regular expressions. /// - public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); + public Regex[] CleanStringRegexes { get; private set; } = []; /// /// Compiles raw regex strings into regexes. diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index 8119a02674..6a07561a06 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -132,7 +132,7 @@ namespace Emby.Naming.Video } } - private class StackMetadata + private sealed class StackMetadata { public StackMetadata(bool isDirectory, bool isNumerical, string partType) { diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index e74755ec32..c69bcfef78 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.AppBase private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) { var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); - if (otherMarkers != null) + if (otherMarkers is not null) { throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); } diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 31ae82d6a3..676bb7f816 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -50,6 +50,8 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask _logger.LogDebug("Cleaning {Number} items with dead parents", numItems); + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + foreach (var itemId in itemIds) { cancellationToken.ThrowIfCancellationRequested(); @@ -95,9 +97,10 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask numComplete++; double percent = numComplete; percent /= numItems; - progress.Report(percent * 100); + subProgress.Report(percent * 100); } + subProgress = new Progress((val) => progress.Report((val / 2) + 50)); var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { @@ -105,7 +108,9 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask await using (transaction.ConfigureAwait(false)) { await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(50); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0db1606ea5..c5dc3b054c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1051,30 +1051,15 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = hasArtist.Artists - // .Except(foundArtists, new DistinctNameComparer()) + dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) + .Where(e => e.Value.Length > 0) .Select(i => { - // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrEmpty(i)) + return new NameGuidPair { - return null; - } - - var artist = _libraryManager.GetArtist(i, new DtoOptions(false) - { - EnableImages = false - }); - if (artist is not null) - { - return new NameGuidPair - { - Name = artist.Name, - Id = artist.Id - }; - } - - return null; + Name = i.Key, + Id = i.Value.First().Id + }; }).Where(i => i is not null).ToArray(); } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f9538fbad6..ca0744a17d 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -37,6 +37,11 @@ namespace Emby.Server.Implementations.Library return false; } + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) + { + return true; + } + // Don't ignore top level folders if (fileInfo.IsDirectory && (parent is AggregateFolder || (parent?.IsTopParent ?? false))) @@ -44,11 +49,6 @@ namespace Emby.Server.Implementations.Library return false; } - if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) - { - return true; - } - if (parent is null) { return false; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 401ca73b80..bafe3ad436 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -50,6 +50,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule return false; } + // Fast path in case the ignore files isn't a symlink and is empty + if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0 + && dirIgnoreFile.Length == 0) + { + return true; + } + // ignore the directory only if the .ignore file is empty // evaluate individual files otherwise return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 25ddade829..fe3a1ce611 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -48,6 +48,8 @@ namespace Emby.Server.Implementations.Library "**/.wd_tv", "**/lost+found/**", "**/lost+found", + "**/subs/**", + "**/subs", // Trickplay files "**/*.trickplay", diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 58a971f62a..ef497726e2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -327,6 +327,45 @@ namespace Emby.Server.Implementations.Library DeleteItem(item, options, parent, notifyParentItem); } + public void DeleteItemsUnsafeFast(IEnumerable items) + { + var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray(); + + foreach (var (item, internalPaths, pathsToDelete) in pathMaps) + { + foreach (var metadataPath in internalPaths) + { + if (!Directory.Exists(metadataPath)) + { + continue; + } + + _logger.LogDebug( + "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + metadataPath, + item.Id); + + try + { + Directory.Delete(metadataPath, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); + } + } + + foreach (var fileSystemInfo in pathsToDelete) + { + DeleteItemPath(item, false, fileSystemInfo); + } + } + + _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]); + } + public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { ArgumentNullException.ThrowIfNull(item); @@ -403,59 +442,7 @@ namespace Emby.Server.Implementations.Library foreach (var fileSystemInfo in item.GetDeletePaths()) { - if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) - { - try - { - _logger.LogInformation( - "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - - if (fileSystemInfo.IsDirectory) - { - Directory.Delete(fileSystemInfo.FullName, true); - } - else - { - File.Delete(fileSystemInfo.FullName); - } - } - catch (DirectoryNotFoundException) - { - _logger.LogInformation( - "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (FileNotFoundException) - { - _logger.LogInformation( - "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (IOException) - { - if (isRequiredForDelete) - { - throw; - } - } - catch (UnauthorizedAccessException) - { - if (isRequiredForDelete) - { - throw; - } - } - } + DeleteItemPath(item, isRequiredForDelete, fileSystemInfo); isRequiredForDelete = false; } @@ -463,17 +450,73 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - _itemRepository.DeleteItem(item.Id); + _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]); _cache.TryRemove(item.Id, out _); foreach (var child in children) { - _itemRepository.DeleteItem(child.Id); _cache.TryRemove(child.Id, out _); } ReportItemRemoved(item, parent); } + private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo) + { + if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) + { + try + { + _logger.LogInformation( + "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + + if (fileSystemInfo.IsDirectory) + { + Directory.Delete(fileSystemInfo.FullName, true); + } + else + { + File.Delete(fileSystemInfo.FullName); + } + } + catch (DirectoryNotFoundException) + { + _logger.LogInformation( + "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (FileNotFoundException) + { + _logger.LogInformation( + "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (IOException) + { + if (isRequiredForDelete) + { + throw; + } + } + catch (UnauthorizedAccessException) + { + if (isRequiredForDelete) + { + throw; + } + } + } + } + private bool IsInternalItem(BaseItem item) { if (!item.IsFileProtocol) @@ -485,7 +528,7 @@ namespace Emby.Server.Implementations.Library { Genre => _configurationManager.ApplicationPaths.GenrePath, MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, - MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath, Person => _configurationManager.ApplicationPaths.PeoplePath, Studio => _configurationManager.ApplicationPaths.StudioPath, Year => _configurationManager.ApplicationPaths.YearPath, @@ -826,6 +869,7 @@ namespace Emby.Server.Implementations.Library if (!folder.ParentId.Equals(rootFolder.Id)) { + rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); folder.ParentId = rootFolder.Id; folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); } @@ -989,6 +1033,11 @@ namespace Emby.Server.Implementations.Library return GetArtist(name, new DtoOptions(true)); } + public IReadOnlyDictionary GetArtists(IReadOnlyList names) + { + return _itemRepository.FindArtists(names); + } + public MusicArtist GetArtist(string name, DtoOptions options) { return CreateItemByName(MusicArtist.GetPath, name, options); @@ -1090,6 +1139,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { + RootFolder.Children = null; await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further @@ -1100,9 +1150,12 @@ namespace Emby.Server.Implementations.Library allowRemoveRoot: removeRoot, cancellationToken: cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); + var rootFolder = GetUserRootFolder(); + rootFolder.Children = null; - await GetUserRootFolder().ValidateChildren( + await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); + + await rootFolder.ValidateChildren( new Progress(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, @@ -1110,18 +1163,24 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var child in GetUserRootFolder().Children.OfType()) + var toDelete = new List(); + foreach (var child in rootFolder.Children!.OfType()) { // If the user has somehow deleted the collection directory, remove the metadata from the database. if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) { - _itemRepository.DeleteItem(collectionFolder.Id); + toDelete.Add(collectionFolder.Id); } else { await child.RefreshMetadata(cancellationToken).ConfigureAwait(false); } } + + if (toDelete.Count > 0) + { + _itemRepository.DeleteItem(toDelete.ToArray()); + } } private async Task PerformLibraryValidation(IProgress progress, CancellationToken cancellationToken) @@ -2027,6 +2086,12 @@ namespace Emby.Server.Implementations.Library } } + if (!File.Exists(image.Path)) + { + _logger.LogWarning("Image not found at {ImagePath}", image.Path); + continue; + } + ImageDimensions size; try { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 1e3b8ea760..750346169f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -657,7 +657,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); + _logger.LogDebug(ex, "Error parsing cached media info."); } finally { diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 28cf695007..e0c8ae371b 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item - .GetRecursiveChildren(user, new InternalItemsQuery(user) - { - IncludeItemTypes = [BaseItemKind.Audio], - DtoOptions = dtoOptions - }) + .GetRecursiveChildren( + user, + new InternalItemsQuery(user) + { + IncludeItemTypes = [BaseItemKind.Audio], + DtoOptions = dtoOptions + }, + out _) .Cast