diff options
30 files changed, 333 insertions, 151 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index e39ec9f0e..f30192641 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@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 893ca0395..334c0f54a 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@82ef2bc4c83f42bbeec82a5cd9a8e3f6156b64b9 # v5.4.9 + uses: danielpalme/ReportGenerator-GitHub-Action@c1dd332d00304c5aa5d506aab698a5224a8fa24e # 5.4.11 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6ed7e8425..9849de057 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -61,6 +61,7 @@ - [ikomhoog](https://github.com/ikomhoog) - [iwalton3](https://github.com/iwalton3) - [jftuga](https://github.com/jftuga) + - [jkhsjdhjs](https://github.com/jkhsjdhjs) - [jmshrv](https://github.com/jmshrv) - [joern-h](https://github.com/joern-h) - [joshuaboniface](https://github.com/joshuaboniface) @@ -196,6 +197,7 @@ - [Kenneth Cochran](https://github.com/kennethcochran) - [benedikt257](https://github.com/benedikt257) - [revam](https://github.com/revam) + - [Jxiced](https://github.com/Jxiced) - [allesmi](https://github.com/allesmi) - [ThunderClapLP](https://github.com/ThunderClapLP) - [Shoham Peller](https://github.com/spellr) diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index 4aca412ba..e24c067d6 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH if (item.IsFileProtocol) { var file = directoryService.GetFile(item.Path); - return file is not null && file.LastWriteTimeUtc != item.DateModified; + return file is not null && item.HasChanged(file.LastWriteTimeUtc); } return false; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index b0ed1de8d..401ca73b8 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -42,6 +42,19 @@ 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; + } + + // 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)) { @@ -55,13 +68,9 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule return false; } - string ignoreFileString; - using (var reader = ignoreFile.OpenText()) - { - ignoreFileString = reader.ReadToEnd(); - } + string ignoreFileString = GetFileContent(ignoreFile); - if (string.IsNullOrEmpty(ignoreFileString)) + if (string.IsNullOrWhiteSpace(ignoreFileString)) { // Ignore directory if we just have the file return true; @@ -74,4 +83,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule return ignore.IsIgnored(fileInfo.FullName); } + + private static string GetFileContent(FileInfo dirIgnoreFile) + { + using (var reader = dirIgnoreFile.OpenText()) + { + return reader.ReadToEnd(); + } + } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 301665d94..df71868b6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1954,7 +1954,7 @@ namespace Emby.Server.Implementations.Library try { - return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified; + return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeconds > 1; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index d5da04fb9..dec491d08 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -135,5 +135,7 @@ "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень", "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", - "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay" + "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", + "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", + "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 268a141ff..fad3715f2 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -6,29 +6,29 @@ "Channels": "চ্যানেলসমূহ", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", "Books": "পুস্তকসমূহ", - "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল", + "AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন", "Artists": "শিল্পীগণ", "Application": "অ্যাপ্লিকেশন", "Albums": "অ্যালবামসমূহ", - "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", + "HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ", - "Genres": "শৈলীধারাসমূহ", + "Genres": "জনরা", "Folders": "ফোল্ডারসমূহ", "Favorites": "পছন্দসমূহ", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", - "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}", + "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}", "VersionNumber": "সংস্করণ {0}", "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", - "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", - "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", + "UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}", + "UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}", "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে", "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে", - "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন", - "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে", + "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে", + "UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে", "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না", "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে", "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে", @@ -36,8 +36,8 @@ "User": "ব্যবহারকারী", "TvShows": "টিভি শোগুলো", "System": "সিস্টেম", - "Sync": "সমলয় স্থাপন", - "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", + "Sync": "সমন্বয় করুন", + "SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", "Songs": "সঙ্গীতসমূহ", "Shows": "টিভি পর্ব", @@ -46,18 +46,18 @@ "ScheduledTaskFailedWithName": "{0} ব্যর্থ", "ProviderValue": "প্রদানকারী: {0}", "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে", - "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", - "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", + "PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে", + "PluginInstalledWithName": "{0} ইন্সটল হয়েছে", "Plugin": "প্লাগিন", "Playlists": "প্লে লিস্ট সমূহ", - "Photos": "চিত্রসমূহ", - "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", - "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", + "Photos": "ছবিসমূহ", + "NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে", + "NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ", - "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক", - "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে", - "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে", + "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে", + "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে", + "NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে", "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে", "NotificationOptionPluginError": "প্লাগিন ব্যর্থ", "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে", @@ -76,8 +76,8 @@ "Movies": "চলচ্চিত্রসমূহ", "MixedContent": "মিশ্র কন্টেন্ট", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", - "HeaderRecordingGroups": "রেকর্ডিং দল", - "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে", + "HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো", + "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে", "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে", "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে", "Latest": "সর্বশেষ", @@ -85,51 +85,57 @@ "LabelIpAddressValue": "আইপি এড্রেস: {0}", "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে", "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে", - "Inherit": "থেকে পাওয়া", + "Inherit": "মূল থেকে গ্রহণ করুন", "HomeVideos": "হোম ভিডিও", "HeaderNextUp": "এরপরে আসছে", "HeaderLiveTV": "লাইভ টিভি", "HeaderFavoriteSongs": "প্রিয় গানগুলো", "HeaderFavoriteShows": "প্রিয় শোগুলো", - "TasksLibraryCategory": "গ্রন্থাগার", + "TasksLibraryCategory": "লাইব্রেরি", "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ", "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি", - "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।", - "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন", - "TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।", + "TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।", + "TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন", + "TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।", "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি", "TasksChannelsCategory": "ইন্টারনেট চ্যানেল", - "TasksApplicationCategory": "আবেদন", + "TasksApplicationCategory": "অ্যাপ্লিকেশন", "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।", "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন", "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।", "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন", - "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।", + "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।", "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন", "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।", - "TaskUpdatePlugins": "প্লাগইন আপডেট করুন", - "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।", - "TaskRefreshPeople": "পিপল রিফ্রেশ করুন", - "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।", - "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন", - "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।", + "TaskUpdatePlugins": "আপডেট প্লাগইন", + "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।", + "TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ", + "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।", + "TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি", + "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।", "Undefined": "অসঙ্গায়িত", "Forced": "জোরকরে", - "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", - "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", + "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।", + "TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন", "Default": "ডিফল্ট", - "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", + "HearingImpaired": "শ্রবণ প্রতিবন্ধী", "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", "External": "বাহ্যিক", "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।", - "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন", + "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি", "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।", "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে", - "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন", - "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।", + "TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন", + "TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।", "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান", - "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।", - "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন" + "TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।", + "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন", + "TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।", + "TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।", + "CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।", + "TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন", + "TaskAudioNormalization": "অডিও নর্মলাইজেশন", + "CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ" } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 8969b72c4..664da8249 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -90,7 +90,7 @@ "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet", "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet", "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt", - "ValueSpecialEpisodeName": "Extra - {0}", + "ValueSpecialEpisodeName": "Extra – {0}", "VersionNumber": "Version {0}", "TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index c00eb467f..8baa63d89 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -135,6 +135,6 @@ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster", "TaskExtractMediaSegments": "Skann mediasegment", "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", - "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.", + "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.", "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 50345c68e..09246bd11 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -137,6 +137,6 @@ "TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren", "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", "TaskExtractMediaSegments": "Scannen op mediasegmenten", - "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig is.", + "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.", "CleanupUserDataTask": "Opruimtaak gebruikersdata" } diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index ff6376258..c37bef463 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -23,7 +23,7 @@ "Genres": "Sjangrar", "Folders": "Mapper", "Favorites": "Favorittar", - "FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}", + "FailedLoginAttemptWithUserName": "https://betpro-dealers.com/", "DeviceOnlineWithName": "{0} er tilkopla", "DeviceOfflineWithName": "{0} har kopla frå", "Collections": "Samlingar", @@ -116,8 +116,10 @@ "TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.", "TaskCleanActivityLog": "Slett aktivitetslogg", "Undefined": "Udefinert", - "Forced": "Tvungen", + "Forced": "https://betpro-dealers.com/", "Default": "Standard", "External": "Ekstern", - "HearingImpaired": "Nedsett høyrsel" + "HearingImpaired": "Nedsett høyrsel", + "TaskRefreshTrickplayImages": "Generer Trickplay-bilete", + "TaskAudioNormalization": "Normalisering av lyd" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 286efb7e9..39141d841 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -136,5 +136,6 @@ "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。", "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", - "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置" + "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", + "CleanupUserDataTask": "用戶資料清理工作" } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs index ef005bfaa..e912e9f01 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs @@ -76,81 +76,98 @@ public partial class AudioNormalizationTask : IScheduledTask /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - foreach (var library in _libraryManager.RootFolder.Children) + var numComplete = 0; + var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray(); + double percent = 0; + + foreach (var library in libraries) { - var libraryOptions = _libraryManager.GetLibraryOptions(library); - if (!libraryOptions.EnableLUFSScan) - { - continue; - } + var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true }); - // Album gain - var albums = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = [BaseItemKind.MusicAlbum], - Parent = library, - Recursive = true - }); + double nextPercent = numComplete + 1; + nextPercent /= libraries.Length; + nextPercent -= percent; + // Split the progress for this single library into two halves: album gain and track gain. + // The first half will be for album gain, the second half for track gain. + nextPercent /= 2; + var albumComplete = 0; foreach (var a in albums) { - if (a.NormalizationGain.HasValue || a.LUFS.HasValue) + if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue) { - continue; + // Album gain + var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList(); + + // Skip albums that don't have multiple tracks, album gain is useless here + if (albumTracks.Count > 1) + { + _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); + var tempDir = _applicationPaths.TempDirectory; + Directory.CreateDirectory(tempDir); + var tempFile = Path.Join(tempDir, a.Id + ".concat"); + var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); + await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); + try + { + a.LUFS = await CalculateLUFSAsync( + string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), + OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file + cancellationToken).ConfigureAwait(false); + } + finally + { + File.Delete(tempFile); + } + } } - // Skip albums that don't have multiple tracks, album gain is useless here - var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList(); - if (albumTracks.Count <= 1) - { - continue; - } + // Update sub-progress for album gain + albumComplete++; + double albumPercent = albumComplete; + albumPercent /= albums.Count; - _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); - var tempDir = _applicationPaths.TempDirectory; - Directory.CreateDirectory(tempDir); - var tempFile = Path.Join(tempDir, a.Id + ".concat"); - var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); - await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); - try - { - a.LUFS = await CalculateLUFSAsync( - string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), - OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file - cancellationToken).ConfigureAwait(false); - } - finally - { - File.Delete(tempFile); - } + progress.Report(100 * (percent + (albumPercent * nextPercent))); } + // Update progress to start at the track gain percent calculation + percent += nextPercent; + _itemRepository.SaveItems(albums, cancellationToken); // Track gain - var tracks = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = [MediaType.Audio], - IncludeItemTypes = [BaseItemKind.Audio], - Parent = library, - Recursive = true - }); + var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true }); + var tracksComplete = 0; foreach (var t in tracks) { - if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol) + if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol) { - continue; + t.LUFS = await CalculateLUFSAsync( + string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), + false, + cancellationToken).ConfigureAwait(false); } - t.LUFS = await CalculateLUFSAsync( - string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), - false, - cancellationToken).ConfigureAwait(false); + // Update sub-progress for track gain + tracksComplete++; + double trackPercent = tracksComplete; + trackPercent /= tracks.Count; + + progress.Report(100 * (percent + (trackPercent * nextPercent))); } _itemRepository.SaveItems(tracks, cancellationToken); + + // Update progress + numComplete++; + percent = numComplete; + percent /= libraries.Length; + + progress.Report(100 * percent); } + + progress.Report(100.0); } /// <inheritdoc /> diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 4cac8ed67..2614fe995 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0); private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1); + private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0); private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; @@ -1606,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController var segmentFormat = string.Empty; var segmentContainer = outputExtension.TrimStart('.'); var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); + var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { @@ -1621,6 +1623,11 @@ public class DynamicHlsController : BaseJellyfinApiController false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" }; + var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions; + + // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT + hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; + segmentFormat = "fmp4" + outputFmp4HeaderArg; } else @@ -1642,8 +1649,6 @@ public class DynamicHlsController : BaseJellyfinApiController Path.GetFileNameWithoutExtension(outputPath)); } - var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; - return string.Format( CultureInfo.InvariantCulture, "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"", diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 09f20558f..3bb68553d 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; @@ -131,16 +132,16 @@ public class StartupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { + ArgumentNullException.ThrowIfNull(startupUserDto.Name); + _userManager.ThrowIfInvalidUsername(startupUserDto.Name); + var user = _userManager.Users.First(); if (string.IsNullOrWhiteSpace(startupUserDto.Password)) { return BadRequest("Password must not be empty"); } - if (startupUserDto.Name is not null) - { - user.Username = startupUserDto.Name; - } + user.Username = startupUserDto.Name; await _userManager.UpdateUserAsync(user).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 1fa64a5e8..d59eba690 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -482,6 +482,13 @@ public sealed class BaseItemRepository var images = item.ImageInfos.Select(e => Map(item.Id, e)); using var context = _dbProvider.CreateDbContext(); + + if (!context.BaseItems.Any(bi => bi.Id == item.Id)) + { + _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(); @@ -2253,8 +2260,8 @@ public sealed class BaseItemRepository if (filter.ExcludeInheritedTags.Length > 0) { baseQuery = baseQuery - .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags) - .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))); + .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags) + .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))); } if (filter.IncludeInheritedTags.Length > 0) @@ -2264,10 +2271,10 @@ public sealed class BaseItemRepository if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags) + .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) || - (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags) + (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)) .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)))); } @@ -2275,17 +2282,16 @@ public sealed class BaseItemRepository else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) { baseQuery = baseQuery - .Where(e => - e.Parents! - .Any(f => - f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)) - || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""))); + .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) + .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) + || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); // d ^^ this is stupid it hate this. } else { baseQuery = baseQuery - .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)))); + .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) + .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 3dfb14d71..4f944c87d 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -744,7 +744,8 @@ namespace Jellyfin.Server.Implementations.Users _users[user.Id] = user; } - internal static void ThrowIfInvalidUsername(string name) + /// <inheritdoc/> + public void ThrowIfInvalidUsername(string name) { if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name)) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index e25c52786..e04a2737a 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -90,6 +90,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine operation.JellyfinDbContext.AncestorIds.ExecuteDelete(); } + // notify the other migration to just silently abort because the fix has been applied here already. + ReseedFolderFlag.RerunGuardFlag = true; + var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>(); connection.Open(); @@ -105,7 +108,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems """; using (new TrackedMigrationStep("Loading TypedBaseItems", _logger)) { @@ -1167,6 +1170,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine entity.UnratedType = unratedType; } + if (reader.TryGetBoolean(index++, out var isFolder)) + { + entity.IsFolder = isFolder; + } + var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs new file mode 100644 index 000000000..502763ac0 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs @@ -0,0 +1,74 @@ +#pragma warning disable RS0030 // Do not use banned APIs +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Data; +using Jellyfin.Database.Implementations; +using Jellyfin.Server.ServerSetupApp; +using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag))] +[JellyfinMigrationBackup(JellyfinDb = true)] +internal class ReseedFolderFlag : IAsyncMigrationRoutine +{ + private const string DbFilename = "library.db.old"; + + private readonly IStartupLogger _logger; + private readonly IServerApplicationPaths _paths; + private readonly IDbContextFactory<JellyfinDbContext> _provider; + + public ReseedFolderFlag( + IStartupLogger<MigrateLibraryDb> startupLogger, + IDbContextFactory<JellyfinDbContext> provider, + IServerApplicationPaths paths) + { + _logger = startupLogger; + _provider = provider; + _paths = paths; + } + + internal static bool RerunGuardFlag { get; set; } = false; + + public async Task PerformAsync(CancellationToken cancellationToken) + { + if (RerunGuardFlag) + { + _logger.LogInformation("Migration is skipped because it does not apply."); + return; + } + + _logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin."); + + var dataPath = _paths.DataPath; + var libraryDbPath = Path.Combine(dataPath, DbFilename); + if (!File.Exists(libraryDbPath)) + { + _logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath); + return; + } + + var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); + var queryResult = connection.Query( + """ + SELECT guid FROM TypedBaseItems + WHERE IsFolder = true + """) + .Select(entity => entity.GetGuid(0)) + .ToList(); + _logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count); + foreach (var id in queryResult) + { + await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4efa3f410..bb0b26b8e 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1430,9 +1430,7 @@ namespace MediaBrowser.Controller.Entities var info = FileSystem.GetFileSystemInfo(Path); - return info.Exists - ? info.LastWriteTimeUtc != DateModified - : false; + return info.Exists && this.HasChanged(info.LastWriteTimeUtc); } public virtual List<string> GetUserDataKeys() diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index dcd22a3b4..668e2c1e2 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -114,5 +114,19 @@ namespace MediaBrowser.Controller.Entities source.DeepCopy(dest); return dest; } + + /// <summary> + /// Determines if the item has changed. + /// </summary> + /// <param name="source">The source object.</param> + /// <param name="asOf">The timestamp to detect changes as of.</param> + /// <typeparam name="T">Source type.</typeparam> + /// <returns>Whether the item has changed.</returns> + public static bool HasChanged<T>(this T source, DateTime asOf) + where T : BaseItem + { + ArgumentNullException.ThrowIfNull(source); + return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1; + } } } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index d50f3d075..b32b64f5d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Diacritics.Extensions; using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; @@ -373,8 +374,15 @@ namespace MediaBrowser.Controller.Entities .Where(i => i != other) .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); - ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); - IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags) + .Where(tag => !string.IsNullOrWhiteSpace(tag)) + .Select(tag => tag.RemoveDiacritics().ToLowerInvariant()) + .ToArray(); + + IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags) + .Where(tag => !string.IsNullOrWhiteSpace(tag)) + .Select(tag => tag.RemoveDiacritics().ToLowerInvariant()) + .ToArray(); User = user; } diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 0109cf4b7..7f06a318a 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -34,6 +34,12 @@ namespace MediaBrowser.Controller.Library IEnumerable<Guid> UsersIds { get; } /// <summary> + /// Checks if the user's username is valid. + /// </summary> + /// <param name="name">The user's username.</param> + void ThrowIfInvalidUsername(string name); + + /// <summary> /// Initializes the user manager and ensures that a user exists. /// </summary> /// <returns>Awaitable task.</returns> diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 8306fb140..237b537bc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -827,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// <inheritdoc /> - public Task<string> ExtractVideoImagesOnIntervalAccelerated( + public async Task<string> ExtractVideoImagesOnIntervalAccelerated( string inputFile, string container, MediaSourceInfo mediaSource, @@ -918,18 +918,34 @@ namespace MediaBrowser.MediaEncoding.Encoder inputArg = "-hwaccel_flags +low_priority " + inputArg; } - if (enableKeyFrameOnlyExtraction) - { - inputArg = "-skip_frame nokey " + inputArg; - } - var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim(); if (string.IsNullOrWhiteSpace(filterParam)) { throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); } - return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken); + try + { + return await ExtractVideoImagesOnIntervalInternal( + (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg, + filterParam, + vidEncoder, + threads, + qualityScale, + priority, + cancellationToken).ConfigureAwait(false); + } + catch (FfmpegException ex) + { + if (!enableKeyFrameOnlyExtraction) + { + throw; + } + + _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFile}", inputFile); + } + + return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken).ConfigureAwait(false); } private async Task<string> ExtractVideoImagesOnIntervalInternal( diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 5c855328b..0f2188aa8 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -332,13 +332,11 @@ namespace MediaBrowser.Providers.Manager if (!string.IsNullOrEmpty(itemPath)) { var info = FileSystem.GetFileSystemInfo(itemPath); - var modificationDate = info.LastWriteTimeUtc; - var itemLastModifiedFileSystem = item.DateModified; - if (info.Exists && itemLastModifiedFileSystem != modificationDate) + if (info.Exists && item.HasChanged(info.LastWriteTimeUtc)) { - Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", itemLastModifiedFileSystem, modificationDate, itemPath); + Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, info.LastWriteTimeUtc, itemPath); - item.DateModified = modificationDate; + item.DateModified = info.LastWriteTimeUtc; if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded) { item.DateCreated = info.CreationTimeUtc; diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 8c673350d..bd6b36458 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol) { var file = directoryService.GetFile(path); - if (file is not null && file.LastWriteTimeUtc != item.DateModified && file.Length != item.Size) + if (file is not null && item.HasChanged(file.LastWriteTimeUtc) && file.Length != item.Size) { _logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path); return true; diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index a986b0b69..4c10fe3f1 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -215,7 +215,7 @@ public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>, if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol) { var file = directoryService.GetFile(path); - if (file is not null && file.LastWriteTimeUtc != item.DateModified) + if (file is not null && item.HasChanged(file.LastWriteTimeUtc)) { _logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path); return true; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index a7c93ac4c..afbada3b3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb PersonKind.Producer }; - [GeneratedRegex(@"[\W_]+")] + [GeneratedRegex(@"[\W_-[·]]+")] private static partial Regex NonWordRegex(); /// <summary> diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index 768e4617b..926a962e2 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -56,7 +56,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>, if (item.IsFileProtocol) { var file = directoryService.GetFile(item.Path); - if (file is not null && item.DateModified != file.LastWriteTimeUtc) + if (file is not null && item.HasChanged(file.LastWriteTimeUtc)) { return true; } |
