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(@".*(\\|\/)(?((?[][ ._-]*[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