aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2026-02-14 12:07:30 +0100
committerGitHub <noreply@github.com>2026-02-14 12:07:30 +0100
commit29582ed461b693368ec56567c2e40cfa20ef4bf5 (patch)
tree04721b833e8e6108c2e13c4f0ea9f4dc7b2ae946 /Emby.Server.Implementations/Library
parentca6d499680f9fbb369844a11eb0e0213b66bb00b (diff)
parent3b6985986709473c69ba785460c702c6bbe3771d (diff)
Merge branch 'master' into issue15137
Diffstat (limited to 'Emby.Server.Implementations/Library')
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs135
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs4
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs60
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs5
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs4
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs24
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs34
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs11
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs2
9 files changed, 193 insertions, 86 deletions
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
index bafe3ad43..ef5d24c70 100644
--- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
@@ -1,6 +1,8 @@
using System;
using System.IO;
+using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO;
@@ -11,28 +13,24 @@ namespace Emby.Server.Implementations.Library;
/// </summary>
public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{
+ private static readonly bool IsWindows = OperatingSystem.IsWindows();
+
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
{
- var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore"));
- if (ignoreFile.Exists)
- {
- return ignoreFile;
- }
-
- var parentDir = directory.Parent;
- if (parentDir is null)
+ for (var current = directory; current is not null; current = current.Parent)
{
- return null;
+ var ignorePath = Path.Join(current.FullName, ".ignore");
+ if (File.Exists(ignorePath))
+ {
+ return new FileInfo(ignorePath);
+ }
}
- return FindIgnoreFile(parentDir);
+ return null;
}
/// <inheritdoc />
- public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
- {
- return IsIgnored(fileInfo, parent);
- }
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
/// <summary>
/// Checks whether or not the file is ignored.
@@ -42,60 +40,101 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{
- if (fileInfo.IsDirectory)
- {
- var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
- if (dirIgnoreFile is null)
- {
- return false;
- }
+ var searchDirectory = fileInfo.IsDirectory
+ ? new DirectoryInfo(fileInfo.FullName)
+ : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
- // 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));
- }
-
- var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
- if (string.IsNullOrEmpty(parentDirPath))
+ if (string.IsNullOrEmpty(searchDirectory.FullName))
{
return false;
}
- var folder = new DirectoryInfo(parentDirPath);
- var ignoreFile = FindIgnoreFile(folder);
+ var ignoreFile = FindIgnoreFile(searchDirectory);
if (ignoreFile is null)
{
return false;
}
- string ignoreFileString = GetFileContent(ignoreFile);
-
- if (string.IsNullOrWhiteSpace(ignoreFileString))
+ // Fast path in case the ignore files isn't a symlink and is empty
+ if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
{
// Ignore directory if we just have the file
return true;
}
- // If file has content, base ignoring off the content .gitignore-style rules
- var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
- var ignore = new Ignore.Ignore();
- ignore.Add(ignoreRules);
+ var content = GetFileContent(ignoreFile);
+ return string.IsNullOrWhiteSpace(content)
+ || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
+ }
- return ignore.IsIgnored(fileInfo.FullName);
+ private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
+ {
+ // If file has content, base ignoring off the content .gitignore-style rules
+ var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ return CheckIgnoreRules(path, rules, isDirectory);
}
- private static string GetFileContent(FileInfo dirIgnoreFile)
+ /// <summary>
+ /// Checks whether a path should be ignored based on an array of ignore rules.
+ /// </summary>
+ /// <param name="path">The path to check.</param>
+ /// <param name="rules">The array of ignore rules.</param>
+ /// <param name="isDirectory">Whether the path is a directory.</param>
+ /// <returns>True if the path should be ignored.</returns>
+ internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
+ => CheckIgnoreRules(path, rules, isDirectory, IsWindows);
+
+ /// <summary>
+ /// Checks whether a path should be ignored based on an array of ignore rules.
+ /// </summary>
+ /// <param name="path">The path to check.</param>
+ /// <param name="rules">The array of ignore rules.</param>
+ /// <param name="isDirectory">Whether the path is a directory.</param>
+ /// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
+ /// <returns>True if the path should be ignored.</returns>
+ internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
{
- using (var reader = dirIgnoreFile.OpenText())
+ var ignore = new Ignore.Ignore();
+
+ // Add each rule individually to catch and skip invalid patterns
+ var validRulesAdded = 0;
+ foreach (var rule in rules)
+ {
+ try
+ {
+ ignore.Add(rule);
+ validRulesAdded++;
+ }
+ catch (RegexParseException)
+ {
+ // Ignore invalid patterns
+ }
+ }
+
+ // If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
+ if (validRulesAdded == 0)
{
- return reader.ReadToEnd();
+ return true;
}
+
+ // Mitigate the problem of the Ignore library not handling Windows paths correctly.
+ // See https://github.com/jellyfin/jellyfin/issues/15484
+ var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
+
+ // Add trailing slash for directories to match "folder/"
+ if (isDirectory)
+ {
+ pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
+ }
+
+ return ignore.IsIgnored(pathToCheck);
+ }
+
+ private static string GetFileContent(FileInfo ignoreFile)
+ {
+ ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
+ return ignoreFile.Exists
+ ? File.ReadAllText(ignoreFile.FullName)
+ : string.Empty;
}
}
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index fe3a1ce61..59ccb9e2c 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library
"**/lost+found",
"**/subs/**",
"**/subs",
+ "**/.snapshots/**",
+ "**/.snapshots",
+ "**/.snapshot/**",
+ "**/.snapshot",
// Trickplay files
"**/*.trickplay",
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a400cb092..f7f5c387e 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -457,6 +457,12 @@ namespace Emby.Server.Implementations.Library
_cache.TryRemove(child.Id, out _);
}
+ if (parent is Folder folder)
+ {
+ folder.Children = null;
+ folder.UserData = null;
+ }
+
ReportItemRemoved(item, parent);
}
@@ -1052,6 +1058,7 @@ namespace Emby.Server.Implementations.Library
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
Name = name,
+ UseRawName = true,
DtoOptions = options
}).Cast<MusicArtist>()
.OrderBy(i => i.IsAccessedByName ? 1 : 0)
@@ -1993,6 +2000,12 @@ namespace Emby.Server.Implementations.Library
RegisterItem(item);
}
+ if (parent is Folder folder)
+ {
+ folder.Children = null;
+ folder.UserData = null;
+ }
+
if (ItemAdded is not null)
{
foreach (var item in items)
@@ -2131,7 +2144,7 @@ namespace Emby.Server.Implementations.Library
item.ValidateImages();
- _itemRepository.SaveImages(item);
+ await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false);
RegisterItem(item);
}
@@ -2150,6 +2163,12 @@ namespace Emby.Server.Implementations.Library
_itemRepository.SaveItems(items, cancellationToken);
+ if (parent is Folder folder)
+ {
+ folder.Children = null;
+ folder.UserData = null;
+ }
+
if (ItemUpdated is not null)
{
foreach (var item in items)
@@ -2183,6 +2202,12 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
+ /// <inheritdoc />
+ public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
+ {
+ await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
+ }
+
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
if (item.IsFileProtocol)
@@ -3176,19 +3201,7 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
- var shortcutFilename = Path.GetFileNameWithoutExtension(path);
-
- var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
-
- while (File.Exists(lnk))
- {
- shortcutFilename += "1";
- lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
- }
-
- _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
-
- RemoveContentTypeOverrides(path);
+ CreateShortcut(virtualFolderPath, pathInfo);
if (saveLibraryOptions)
{
@@ -3353,5 +3366,24 @@ namespace Emby.Server.Implementations.Library
return item is UserRootFolder || item.IsVisibleStandalone(user);
}
+
+ public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
+ {
+ var path = pathInfo.Path;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ var shortcutFilename = Path.GetFileNameWithoutExtension(path);
+
+ var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+
+ while (File.Exists(lnk))
+ {
+ shortcutFilename += "1";
+ lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+ }
+
+ _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
+ RemoveContentTypeOverrides(path);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 750346169..c667fb060 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />>
public MediaProtocol GetPathProtocol(string path)
{
+ if (string.IsNullOrEmpty(path))
+ {
+ return MediaProtocol.File;
+ }
+
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Rtsp;
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index e0c8ae371..e19ad3ef6 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
- return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
+ var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
+
+ return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 21e7079d8..fc63251ad 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -37,15 +37,25 @@ namespace Emby.Server.Implementations.Library
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;
- if (attributeIndex > 0
- && str[attributeIndex - 1] == '['
- && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
+ if (attributeIndex > 0)
{
- var closingIndex = str[attributeEnd..].IndexOf(']');
- // Must be at least 1 character before the closing bracket.
- if (closingIndex > 1)
+ var attributeOpener = str[attributeIndex - 1];
+ var attributeCloser = attributeOpener switch
{
- return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+ '[' => ']',
+ '(' => ')',
+ '{' => '}',
+ _ => '\0'
+ };
+ if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
+ {
+ var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
+
+ // Must be at least 1 character before the closing bracket.
+ if (closingIndex > 1)
+ {
+ return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 464a548ab..1e885aad6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -5,12 +5,12 @@
using System;
using System.IO;
using System.Linq;
+using Emby.Naming.Book;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers.Books
{
@@ -35,17 +35,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var extension = Path.GetExtension(args.Path.AsSpan());
- if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
- // It's a book
- return new Book
- {
- Path = args.Path,
- IsInMixedFolder = true
- };
+ return null;
}
- return null;
+ var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path));
+
+ return new Book
+ {
+ Path = args.Path,
+ Name = result.Name ?? string.Empty,
+ IndexNumber = result.Index,
+ ProductionYear = result.Year,
+ SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
+ IsInMixedFolder = true,
+ };
}
private Book GetBook(ItemResolveArgs args)
@@ -59,15 +64,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
StringComparison.OrdinalIgnoreCase);
}).ToList();
- // Don't return a Book if there is more (or less) than one document in the directory
+ // directory is only considered a book when it contains exactly one supported file
+ // other library structures with multiple books to a directory will get picked up as individual files
if (bookFiles.Count != 1)
{
return null;
}
+ var result = BookFileNameParser.Parse(Path.GetFileName(args.Path));
+
return new Book
{
- Path = bookFiles[0].FullName
+ Path = bookFiles[0].FullName,
+ Name = result.Name ?? string.Empty,
+ IndexNumber = result.Index,
+ ProductionYear = result.Year,
+ SeriesName = result.SeriesName ?? string.Empty,
};
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 333c8c34b..98e8f5350 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -369,13 +369,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
- if (!justName.IsEmpty)
+ var tmdbid = justName.GetAttributeValue("tmdbid");
+
+ // If not in a mixed folder and ID not found in folder path, check filename
+ if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
{
- // Check for TMDb id
- var tmdbid = justName.GetAttributeValue("tmdbid");
- item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
+ tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
}
+ item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
+
if (!string.IsNullOrEmpty(item.Path))
{
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 9d81b835c..c68211859 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
}
- if (query.Limit.HasValue)
+ if (query.Limit.HasValue && query.Limit.Value > 0)
{
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
}