From acb9da6f93829eaef356d3effd1872798c778adb Mon Sep 17 00:00:00 2001 From: Nilesh Patel Date: Wed, 10 Dec 2025 12:23:05 -0800 Subject: Add curly brace and parentheses support for parsing attribute values from paths --- .../Library/PathExtensions.cs | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 21e7079d88..fc63251ad0 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(); + } } } -- cgit v1.2.3 From dd480f96cdd341c8a986eb4cb7133eff3d535f63 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 14 Dec 2025 00:29:28 +0900 Subject: parse more information from book filenames (#15655) --- Emby.Naming/Book/BookFileNameParser.cs | 75 ++++++++++++++++++++++ Emby.Naming/Book/BookFileNameParserResult.cs | 41 ++++++++++++ .../Library/Resolvers/Books/BookResolver.cs | 34 ++++++---- 3 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 Emby.Naming/Book/BookFileNameParser.cs create mode 100644 Emby.Naming/Book/BookFileNameParserResult.cs (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Naming/Book/BookFileNameParser.cs b/Emby.Naming/Book/BookFileNameParser.cs new file mode 100644 index 0000000000..28625f16de --- /dev/null +++ b/Emby.Naming/Book/BookFileNameParser.cs @@ -0,0 +1,75 @@ +using System.Text.RegularExpressions; + +namespace Emby.Naming.Book +{ + /// + /// Helper class to retrieve basic metadata from a book filename. + /// + public static class BookFileNameParser + { + private const string NameMatchGroup = "name"; + private const string IndexMatchGroup = "index"; + private const string YearMatchGroup = "year"; + private const string SeriesNameMatchGroup = "seriesName"; + + private static readonly Regex[] _nameMatches = + [ + // seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required + new Regex(@"^(?.+?)((\s\((?[0-9]{4})\))?)\s#(?[0-9]+)((\s\(of\s(?[0-9]+)\))?)((\s\((?[0-9]{4})\))?)$"), + new Regex(@"^(?.+?)\s\((?.+?),\s#(?[0-9]+)\)((\s\((?[0-9]{4})\))?)$"), + new Regex(@"^(?[0-9]+)\s\-\s(?.+?)((\s\((?[0-9]{4})\))?)$"), + new Regex(@"(?.*)\((?[0-9]{4})\)"), + // last resort matches the whole string as the name + new Regex(@"(?.*)") + ]; + + /// + /// Parse a filename name to retrieve the book name, series name, index, and year. + /// + /// Book filename to parse for information. + /// Returns object. + public static BookFileNameParserResult Parse(string? name) + { + var result = new BookFileNameParserResult(); + + if (name == null) + { + return result; + } + + foreach (var regex in _nameMatches) + { + var match = regex.Match(name); + + if (!match.Success) + { + continue; + } + + if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success) + { + result.Name = nameGroup.Value.Trim(); + } + + if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index)) + { + result.Index = index; + } + + if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year)) + { + result.Year = year; + } + + if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success) + { + result.SeriesName = seriesGroup.Value.Trim(); + } + + break; + } + + return result; + } + } +} diff --git a/Emby.Naming/Book/BookFileNameParserResult.cs b/Emby.Naming/Book/BookFileNameParserResult.cs new file mode 100644 index 0000000000..f29716b9e3 --- /dev/null +++ b/Emby.Naming/Book/BookFileNameParserResult.cs @@ -0,0 +1,41 @@ +using System; + +namespace Emby.Naming.Book +{ + /// + /// Data object used to pass metadata parsed from a book filename. + /// + public class BookFileNameParserResult + { + /// + /// Initializes a new instance of the class. + /// + public BookFileNameParserResult() + { + Name = null; + Index = null; + Year = null; + SeriesName = null; + } + + /// + /// Gets or sets the name of the book. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the book index. + /// + public int? Index { get; set; } + + /// + /// Gets or sets the publication year. + /// + public int? Year { get; set; } + + /// + /// Gets or sets the series name. + /// + public string? SeriesName { get; set; } + } +} diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 464a548ab9..1e885aad6e 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, }; } } -- cgit v1.2.3 From 771b0a7eabc7c3082dd5b328f121417385a1fc99 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sat, 13 Dec 2025 08:43:49 -0700 Subject: Library: Async the SaveImages function (#15718) --- .../Library/LibraryManager.cs | 2 +- .../Item/BaseItemRepository.cs | 32 +++++++++++++++------- .../Persistence/IItemRepository.cs | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cab87e53de..30c3e89b49 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2143,7 +2143,7 @@ namespace Emby.Server.Implementations.Library item.ValidateImages(); - _itemRepository.SaveImages(item); + await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false); RegisterItem(item); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index dfe46ef8f6..9851d53c40 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -547,22 +547,34 @@ public sealed class BaseItemRepository } /// - public void SaveImages(BaseItemDto item) + public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(item); - var images = item.ImageInfos.Select(e => Map(item.Id, e)); - using var context = _dbProvider.CreateDbContext(); + var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray(); - if (!context.BaseItems.Any(bi => bi.Id == item.Id)) + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); - return; - } + if (!await context.BaseItems + .AnyAsync(bi => bi.Id == item.Id, cancellationToken) + .ConfigureAwait(false)) + { + _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); + return; + } - context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); - context.BaseItemImageInfos.AddRange(images); - context.SaveChanges(); + await context.BaseItemImageInfos + .Where(e => e.ItemId == item.Id) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + + await context.BaseItemImageInfos + .AddRangeAsync(images, cancellationToken) + .ConfigureAwait(false); + + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0026ab2b5f..00c492742a 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -33,7 +33,7 @@ public interface IItemRepository /// The cancellation token. void SaveItems(IReadOnlyList items, CancellationToken cancellationToken); - void SaveImages(BaseItem item); + Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); /// /// Retrieves the item. -- cgit v1.2.3 From 55570043759bcfa3c76df7e94d3303257171c970 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 28 Dec 2025 07:22:17 -0500 Subject: Backport pull request #15689 from jellyfin/release-10.11.z Use original name for MusicAritist matching Original-merge: 4c5a3fbff34a603ff0344e0b42d07bc17f31f92c Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 11 +++++++++-- MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 30c3e89b49..f35d85f659 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1058,6 +1058,7 @@ namespace Emby.Server.Implementations.Library { IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, + UseRawName = true, DtoOptions = options }).Cast() .OrderBy(i => i.IsAccessedByName ? 1 : 0) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 6b060430e6..98072918c4 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1987,8 +1987,15 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.Name)) { - var cleanName = GetCleanValue(filter.Name); - baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + if (filter.UseRawName == true) + { + baseQuery = baseQuery.Where(e => e.Name == filter.Name); + } + else + { + var cleanName = GetCleanValue(filter.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } } // These are the same, for now diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index b32b64f5da..076a592922 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities public string? Name { get; set; } + public bool? UseRawName { get; set; } + public string? Person { get; set; } public Guid[] PersonIds { get; set; } -- cgit v1.2.3 From b9cf26db2f3d219340a17951c289a230d0ccf31a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 28 Dec 2025 07:22:20 -0500 Subject: Backport pull request #15746 from jellyfin/release-10.11.z Skip invalid ignore rules Original-merge: 6e60634c9f078cc01e343b07a0a6b2a5c230478c Merged-by: crobibero Backported-by: Bond_009 --- .../Library/DotIgnoreIgnoreRule.cs | 48 +++++++++++- .../Library/DotIgnoreIgnoreRuleTest.cs | 87 +++++++++++++++++----- 2 files changed, 115 insertions(+), 20 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 473ff8e1d7..ef5d24c70f 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -70,12 +71,55 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule { // 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); + } + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory) + => CheckIgnoreRules(path, rules, isDirectory, IsWindows); + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// Whether to normalize backslashes to forward slashes (for Windows paths). + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath) + { var ignore = new Ignore.Ignore(); - ignore.Add(rules); + + // 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 true; + } // Mitigate the problem of the Ignore library not handling Windows paths correctly. // See https://github.com/jellyfin/jellyfin/issues/15484 - var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + var pathToCheck = normalizePath ? path.NormalizePath('/') : path; // Add trailing slash for directories to match "folder/" if (isDirectory) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index d677c9f091..a7bbef7ed4 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,30 +1,81 @@ +using Emby.Server.Implementations.Library; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; public class DotIgnoreIgnoreRuleTest { - [Fact] - public void Test() + private static readonly string[] _rule1 = ["SPs"]; + private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"]; + private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"]; + private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"]; + + public static TheoryData CheckIgnoreRulesTestData => + new() + { + // Basic pattern matching + { _rule1, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule1, "cd/sps/ffffff.mkv", false, true }, + { _rule1, "/cd/sps/ffffff.mkv", false, true }, + + // Negate pattern + { _rule2, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule2, "cd/sps/ffffff.mkv", false, true }, + { _rule2, "/cd/sps/ffffff.mkv", false, true }, + { _rule2, "f:/cd/sps/thebestshot.mkv", false, false }, + { _rule2, "cd/sps/thebestshot.mkv", false, false }, + { _rule2, "/cd/sps/thebestshot.mkv", false, false }, + + // Mixed valid and invalid patterns - skips invalid, applies valid + { _rule3, "test.txt", false, true }, + { _rule3, "videos/movie.mp4", false, true }, + { _rule3, "movie.mkv", false, true }, + { _rule3, "test.mp3", false, false }, + + // Only invalid patterns - falls back to ignore all + { _rule4, "any-file.txt", false, true }, + { _rule4, "any/path/to/file.mkv", false, true }, + }; + + public static TheoryData WindowsPathNormalizationTestData => + new() + { + // Windows paths with backslashes - should match when normalizePath is true + { _rule1, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule1, @"D:\media\sps\movie.mkv", false, true }, + { _rule1, @"\\server\share\sps\file.mkv", false, true }, + + // Negate pattern with Windows paths + { _rule2, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule2, @"C:\cd\sps\thebestshot.mkv", false, false }, + + // Directory matching with Windows paths + { _rule3, @"C:\videos\movie.mp4", false, true }, + { _rule3, @"D:\documents\test.txt", false, true }, + { _rule3, @"E:\music\song.mp3", false, false }, + }; + + [Theory] + [MemberData(nameof(CheckIgnoreRulesTestData))] + public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored) + { + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory)); + } + + [Theory] + [MemberData(nameof(WindowsPathNormalizationTestData))] + public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); + // With normalizePath=true, backslashes should be converted to forward slashes + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true)); } - [Fact] - public void TestNegatePattern() + [Theory] + [InlineData(@"C:\cd\sps\ffffff.mkv")] + [InlineData(@"D:\media\sps\movie.mkv")] + public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - ignore.Add("!thebestshot.mkv"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); - Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv")); + // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes + Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } } -- cgit v1.2.3 From 09edca8b7a9174c374a7d03bb1ec3aea32d02ffd Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:38 -0500 Subject: Backport pull request #15899 from jellyfin/release-10.11.z Fix watched state not kept on Media replace/rename Original-merge: 8433b6d8a41f66f6eef36bb950927c6a6afa1a36 Merged-by: joshuaboniface Backported-by: Bond_009 --- CONTRIBUTORS.md | 1 + .../Library/LibraryManager.cs | 6 ++++ .../Item/BaseItemRepository.cs | 37 ++++++++++++++-------- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++ MediaBrowser.Controller/Library/ILibraryManager.cs | 8 +++++ .../Persistence/IItemRepository.cs | 8 +++++ MediaBrowser.Providers/Manager/MetadataService.cs | 11 +++++-- 7 files changed, 57 insertions(+), 17 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 171509382d..1770db60be 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -210,6 +210,7 @@ - [bjorntp](https://github.com/bjorntp) - [martenumberto](https://github.com/martenumberto) - [ZeusCraft10](https://github.com/ZeusCraft10) + - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) # Emby Contributors diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f35d85f659..bdf04edc2e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync([item], parent, updateReason, cancellationToken); + /// + 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) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 3b3d3c4f43..646a9c4836 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -624,7 +624,6 @@ public sealed class BaseItemRepository var ids = tuples.Select(f => f.Item.Id).ToArray(); var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); - var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray(); foreach (var item in tuples) { @@ -658,19 +657,6 @@ public sealed class BaseItemRepository context.SaveChanges(); - foreach (var item in newItems) - { - // reattach old userData entries - var userKeys = item.UserDataKey.ToArray(); - var retentionDate = (DateTime?)null; - context.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdate(e => e - .SetProperty(f => f.ItemId, item.Item.Id) - .SetProperty(f => f.RetentionDate, retentionDate)); - } - var itemValueMaps = tuples .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) .ToArray(); @@ -766,6 +752,29 @@ public sealed class BaseItemRepository transaction.Commit(); } + /// + public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(item); + cancellationToken.ThrowIfCancellationRequested(); + + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + await using (dbContext.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + } + } + /// public BaseItemDto? RetrieveItem(Guid id) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d9d2d0e3a8..7586b99e77 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); + public async Task ReattachUserDataAsync(CancellationToken cancellationToken) => + await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false); + /// /// Validates that images within the item are still on the filesystem. /// diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index fcc5ed672a..675812ac23 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library /// Returns a Task that can be awaited. Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 00c492742a..bf80b7d0a8 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -35,6 +35,14 @@ public interface IItemRepository Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index a2102ca9cd..e9cb46eab5 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager if (isFirstRefresh) { - await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false); } // Next run metadata providers @@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false); } return updateType; @@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager } } - protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken) { await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); + if (reattachUserData) + { + await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); + } + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; -- cgit v1.2.3 From afcaec0a894df038f8b88a517a01bace0d3c237c Mon Sep 17 00:00:00 2001 From: Collin-Swish <79892877+Collin-Swish@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:39 -0500 Subject: Backport pull request #15965 from jellyfin/release-10.11.z Add mblink creation logic to library update endpoint. Original-merge: 22d593b8e986ecdb42fb1e618bfcf833b0a6f118 Merged-by: crobibero Backported-by: Bond_009 --- .../Library/LibraryManager.cs | 33 +++++++++++++--------- .../Controllers/LibraryStructureController.cs | 11 ++++++++ MediaBrowser.Controller/Library/ILibraryManager.cs | 7 +++++ 3 files changed, 38 insertions(+), 13 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bdf04edc2e..f7f5c387e1 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3201,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) { @@ -3378,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/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 2a885662b5..117811429a 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController return NotFound(); } + LibraryOptions options = item.GetLibraryOptions(); + foreach (var mediaPath in request.LibraryOptions!.PathInfos) + { + if (options.PathInfos.Any(i => i.Path == mediaPath.Path)) + { + continue; + } + + _libraryManager.CreateShortcut(item.Path, mediaPath); + } + item.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 675812ac23..df1c98f3f7 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -660,5 +660,12 @@ namespace MediaBrowser.Controller.Library /// This exists so plugins can trigger a library scan. /// void QueueLibraryScan(); + + /// + /// Add mblink file for a media path. + /// + /// The path to the virtualfolder. + /// The new virtualfolder. + public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo); } } -- cgit v1.2.3 From c9b7c5bb5665d2eb02381fa12dcce786fee9ae3d Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:43 -0500 Subject: Backport pull request #16029 from jellyfin/release-10.11.z Skip hidden directories and .ignore paths in library monitoring Original-merge: 2cb7fb52d2221d9daa39206089b578c2c0fcb549 Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 6 ++++++ Emby.Server.Implementations/Library/IgnorePatterns.cs | 1 + .../Library/IgnorePatternsTests.cs | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index d87ad729ee..7cff2a25b6 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO return; } + var fileInfo = _fileSystem.GetFileSystemInfo(path); + if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null)) + { + return; + } + // Ignore certain files, If the parent of an ignored path has a change event, ignore that too foreach (var i in _tempIgnoredPaths.Keys) { diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index fe3a1ce611..5fac2f6b0a 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", + "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 07061cfc77..4cb6cb9607 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", false)] + [InlineData("/media/.hiddendir/file.mp4", true)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } -- cgit v1.2.3 From 0c274af72c1d223c0fea7f4f4ac99e04f1e1e904 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:28 -0500 Subject: Backport pull request #16077 from jellyfin/release-10.11.z Revert hidden directory ignore pattern Original-merge: 644327eb762a907328c68ab9f5d61a151cd96897 Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 5 ++++- .../Library/IgnorePatternsTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5fac2f6b0a..59ccb9e2c7 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", @@ -83,7 +87,6 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", - "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 4cb6cb9607..07061cfc77 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/.hiddendir/file.mp4", false)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } -- cgit v1.2.3 From 946c6b9981145d73a6cd64fc6fbcbd6d5b6961ae Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 11 Mar 2026 21:20:14 +0100 Subject: Return BadRequest when an invalid set of filters is given --- .../Library/LibraryManager.cs | 2 +- Jellyfin.Api/Controllers/ArtistsController.cs | 68 +-------------------- Jellyfin.Api/Controllers/ChannelsController.cs | 70 +-------------------- Jellyfin.Api/Controllers/ItemsController.cs | 34 +---------- .../Entities/InternalItemsQuery.cs | 71 ++++++++++++++++++++++ .../Entities/InternalItemsQueryTests.cs | 26 ++++++++ 6 files changed, 104 insertions(+), 167 deletions(-) create mode 100644 tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f7f5c387e1..eee87c4d8b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2289,7 +2289,7 @@ namespace Emby.Server.Implementations.Library if (item is null) { - return new List(); + return []; } return GetCollectionFoldersInternal(item, allUserRootChildren); diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 642790f942..99b0fde06d 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -187,39 +187,7 @@ public class ArtistsController : BaseJellyfinApiController }).Where(i => i is not null).Select(i => i!.Id).ToArray(); } - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } + query.ApplyFilters(filters); var result = _libraryManager.GetArtists(query); @@ -390,39 +358,7 @@ public class ArtistsController : BaseJellyfinApiController }).Where(i => i is not null).Select(i => i!.Id).ToArray(); } - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } + query.ApplyFilters(filters); var result = _libraryManager.GetAlbumArtists(query); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 880b3a82d4..0d85b3a0db 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -136,45 +136,13 @@ public class ChannelsController : BaseJellyfinApiController { Limit = limit, StartIndex = startIndex, - ChannelIds = new[] { channelId }, + ChannelIds = [channelId], ParentId = folderId ?? Guid.Empty, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), DtoOptions = new DtoOptions { Fields = fields } }; - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - } - } + query.ApplyFilters(filters); return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false); } @@ -215,39 +183,7 @@ public class ChannelsController : BaseJellyfinApiController DtoOptions = new DtoOptions { Fields = fields } }; - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - } - } + query.ApplyFilters(filters); return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 9674ecd092..091a0c8c73 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -386,39 +386,7 @@ public class ItemsController : BaseJellyfinApiController query.CollapseBoxSetItems = false; } - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } + query.ApplyFilters(filters); // Filter by Series Status if (seriesStatus.Length != 0) diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 076a592922..ecbeefbb9d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -10,6 +10,7 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities { @@ -388,5 +389,75 @@ namespace MediaBrowser.Controller.Entities User = user; } + + public void ApplyFilters(ItemFilter[] filters) + { + static void ThrowConflictingFilters() + => throw new ArgumentException("Conflicting filters", nameof(filters)); + + foreach (var filter in filters) + { + switch (filter) + { + case ItemFilter.IsFolder: + if (filters.Contains(ItemFilter.IsNotFolder)) + { + ThrowConflictingFilters(); + } + + IsFolder = true; + break; + case ItemFilter.IsNotFolder: + if (filters.Contains(ItemFilter.IsFolder)) + { + ThrowConflictingFilters(); + } + + IsFolder = false; + break; + case ItemFilter.IsUnplayed: + if (filters.Contains(ItemFilter.IsPlayed)) + { + ThrowConflictingFilters(); + } + + IsPlayed = false; + break; + case ItemFilter.IsPlayed: + if (filters.Contains(ItemFilter.IsUnplayed)) + { + ThrowConflictingFilters(); + } + + IsPlayed = true; + break; + case ItemFilter.IsFavorite: + IsFavorite = true; + break; + case ItemFilter.IsResumable: + IsResumable = true; + break; + case ItemFilter.Likes: + if (filters.Contains(ItemFilter.Dislikes)) + { + ThrowConflictingFilters(); + } + + IsLiked = true; + break; + case ItemFilter.Dislikes: + if (filters.Contains(ItemFilter.Likes)) + { + ThrowConflictingFilters(); + } + + IsLiked = false; + break; + case ItemFilter.IsFavoriteOrLikes: + IsFavoriteOrLiked = true; + break; + } + } + } } } diff --git a/tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs b/tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs new file mode 100644 index 0000000000..7093b25006 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs @@ -0,0 +1,26 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Querying; +using Xunit; + +namespace Jellyfin.Controller.Tests.Entities; + +public class InternalItemsQueryTests +{ + public static TheoryData ApplyFilters_Invalid() + { + var data = new TheoryData(); + data.Add([ItemFilter.IsFolder, ItemFilter.IsNotFolder]); + data.Add([ItemFilter.IsPlayed, ItemFilter.IsUnplayed]); + data.Add([ItemFilter.Likes, ItemFilter.Dislikes]); + return data; + } + + [Theory] + [MemberData(nameof(ApplyFilters_Invalid))] + public void ApplyFilters_Invalid_ThrowsArgumentException(ItemFilter[] filters) + { + var query = new InternalItemsQuery(); + Assert.Throws(() => query.ApplyFilters(filters)); + } +} -- cgit v1.2.3 From 995d56d5ff37c010f91780f70a66f75ef95c5f25 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 19 Mar 2026 14:31:29 -0500 Subject: Recognize ".m4b", ".m4a", ".aac", ".flac", ".mp3", and ".opus" as an audio-book formats (#15377) * Recognize ".m4b" as an audio-book format - Has the resolver recognize the ".m4b" format as book - Jellyfin reverts to seeing the file as a music file without this check * Recognize ".m4a", ".aac", ".flac", and ".mp3" as an audio-book formats - All the formats supported in the docs will now be marked as type "Book" * Add ".opus" as a supported Audiobook format --------- Co-authored-by: Louis Sandoval --- Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 1e885aad6e..3ee1c757f2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : ItemResolver { - private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" }; protected override Book Resolve(ItemResolveArgs args) { -- cgit v1.2.3 From 921a364bb0b94b54f7a3215accdb0bc5f51ef9e7 Mon Sep 17 00:00:00 2001 From: furdiburd <93724729+furdiburd@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:38:46 +0200 Subject: Add ignore patterns for Hungarian sample files (#16238) * Add ignore patterns for Hungarian sample files Added ignore patterns for Hungarian sample files. * Removed leftover spaces --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 59ccb9e2c7..197ec42c50 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -31,6 +31,20 @@ namespace Emby.Server.Implementations.Library "**/*.sample.?????", "**/sample/*", + // Avoid adding Hungarian sample files + // https://github.com/jellyfin/jellyfin/issues/16237 + "**/minta.?", + "**/minta.??", + "**/minta.???", // Matches minta.mkv + "**/minta.????", // Matches minta.webm + "**/minta.?????", + "**/*.minta.?", + "**/*.minta.??", + "**/*.minta.???", + "**/*.minta.????", + "**/*.minta.?????", + "**/minta/*", + // Directories "**/metadata/**", "**/metadata", -- cgit v1.2.3 From 5bad7b8ae324ccf0d4eb3051d0944edfddfdb50b Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 14 Apr 2026 12:38:01 -0400 Subject: Fix artist metadata not being fetched on initial library scan (#16606) * Fix artist metadata not being fetched on initial library scan * Update Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs Co-authored-by: Bond-009 --------- Co-authored-by: Bond-009 --- .../Library/Validators/ArtistsValidator.cs | 11 ++++++++++- MediaBrowser.Providers/Manager/ProviderManager.cs | 7 +++++++ .../Plugins/AudioDb/AudioDbArtistProvider.cs | 6 +++++- .../Plugins/MusicBrainz/MusicBrainzArtistProvider.cs | 6 +++++- 4 files changed, 27 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7cc851b73b..ef20ae9bca 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -50,6 +50,10 @@ public class ArtistsValidator public async Task Run(IProgress progress, CancellationToken cancellationToken) { var names = _itemRepo.GetAllArtistNames(); + var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist] + }).ToHashSet(); var numComplete = 0; var count = names.Count; @@ -59,8 +63,13 @@ public class ArtistsValidator try { var item = _libraryManager.GetArtist(name); + var isNew = !existingArtistIds.Contains(item.Id); + var neverRefreshed = item.DateLastRefreshed == default; - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + if (isNew || neverRefreshed) + { + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException) { diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f8e2aece1f..0bab73180f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -487,6 +487,13 @@ namespace MediaBrowser.Providers.Manager return true; } + // Artists without a folder structure that are derived from metadata have no real path in the library, + // so GetLibraryOptions returns null. Allow all providers through rather than blocking them. + if (item is MusicArtist && libraryTypeOptions is null) + { + return true; + } + return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 00bd96282c..d8cb6b4b24 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -125,7 +125,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (string.IsNullOrWhiteSpace(overview)) { - overview = result.strBiographyEN; + overview = string.IsNullOrWhiteSpace(result.strBiographyEN) + ? result.strBiography + : result.strBiographyEN; } item.Overview = (overview ?? string.Empty).StripHtml(); @@ -224,6 +226,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string strTwitter { get; set; } + public string strBiography { get; set; } + public string strBiographyEN { get; set; } public string strBiographyDE { get; set; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 1323d2604a..9df21596c5 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// /// MusicBrainz artist provider. /// -public class MusicBrainzArtistProvider : IRemoteMetadataProvider, IDisposable +public class MusicBrainzArtistProvider : IRemoteMetadataProvider, IDisposable, IHasOrder { private readonly ILogger _logger; private Query _musicBrainzQuery; @@ -42,6 +42,10 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider public string Name => "MusicBrainz"; + /// + /// Runs first to populate the MusicBrainz artist ID used by downstream providers. + public int Order => 0; + private void ReloadConfig(object? sender, BasePluginConfiguration e) { var configuration = (PluginConfiguration)e; -- cgit v1.2.3