diff options
59 files changed, 492 insertions, 880 deletions
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 889133aed..5dee03ef8 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -41,7 +41,15 @@ jobs: - name: Checkout repository uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 with: - ref: ${{ github.base_ref }} + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + - name: Checkout common ancestor + run: | + git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} + git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* + ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }}) + git checkout --progress --force $ANCESTOR_REF - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: @@ -95,7 +103,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 # v2 + uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 1c6fe1492..897b7014a 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -5,13 +5,14 @@ on: - cron: '30 1 * * *' workflow_dispatch: -permissions: {} +permissions: + issues: write jobs: stale: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6 + - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 @@ -22,7 +23,7 @@ jobs: stale-issue-label: stale stale-issue-message: |- This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. - + If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. - + This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index e9041186f..bea7a5a0d 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -195,7 +195,7 @@ namespace Emby.Dlna.Didl { var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user); - streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions + streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions { ItemId = video.Id, MediaSources = sources.ToArray(), @@ -537,7 +537,7 @@ namespace Emby.Dlna.Didl { var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user); - streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions + streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions { ItemId = audio.Id, MediaSources = sources.ToArray(), diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 4cda1d8b7..7b1f942c5 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions + StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions { ItemId = item.Id, MediaSources = mediaSources, @@ -605,7 +605,7 @@ namespace Emby.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions + StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions { ItemId = item.Id, MediaSources = mediaSources, diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index 23437f1bd..54a0a87a8 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -94,6 +94,12 @@ namespace Emby.Dlna.Profiles new SubtitleProfile { + Format = "sup", + Method = SubtitleDeliveryMethod.External + }, + + new SubtitleProfile + { Format = "srt", Method = SubtitleDeliveryMethod.Embed }, @@ -142,6 +148,12 @@ namespace Emby.Dlna.Profiles new SubtitleProfile { + Format = "sup", + Method = SubtitleDeliveryMethod.Embed + }, + + new SubtitleProfile + { Format = "subrip", Method = SubtitleDeliveryMethod.Embed }, diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 0119fa38c..54f62a157 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -153,7 +153,7 @@ namespace Emby.Naming.Common CleanStrings = new[] { - @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", + @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"^(?<cleaned>.+?)(\[.*\])", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", @@ -169,6 +169,7 @@ namespace Emby.Naming.Common ".srt", ".ssa", ".sub", + ".sup", ".vtt", }; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0c6c31982..5103b1fbf 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -574,8 +574,7 @@ namespace Emby.Server.Implementations.Dto .Where(i => user is null ? true : i.IsVisible(user)) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); for (var i = 0; i < people.Count; i++) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 25a7029c9..05d0a9b79 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -282,19 +282,16 @@ namespace Emby.Server.Implementations.EntryPoints { // Remove dupes in case some were saved multiple times var foldersAddedTo = _foldersAddedTo - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .ToList(); var foldersRemovedFrom = _foldersRemovedFrom - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .ToList(); var itemsUpdated = _itemsUpdated .Where(i => !_itemsAdded.Contains(i)) - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .ToList(); SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 42c8f24a1..e724618b3 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -123,8 +123,7 @@ namespace Emby.Server.Implementations.EntryPoints var user = _userManager.GetUserById(userId); var dtoList = changedItems - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .Select(i => { var dto = _userDataManager.GetUserDataDto(i, user); diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index ec8590929..0ad81b653 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -133,8 +133,7 @@ namespace Emby.Server.Implementations.IO .Distinct(StringComparer.OrdinalIgnoreCase) .Select(GetAffectedBaseItem) .Where(item => item is not null) - .GroupBy(x => x!.Id) // Removed null values in the previous .Where() - .Select(x => x.First())!; + .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() foreach (var item in itemsToRefresh) { diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 4b999d40b..f67a02be8 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.IO .OfType<Folder>() .SelectMany(f => f.PhysicalLocations) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i); + .Order(); foreach (var path in paths) { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 82690f8a9..0bd5fdce0 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -81,8 +81,7 @@ namespace Emby.Server.Implementations.Images } return i; - }).GroupBy(x => x.Id) - .Select(x => x.First()); + }).DistinctBy(x => x.Id); List<BaseItem> returnItems; if (isUsingCollectionStrip) diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs index 580151287..3326d21ac 100644 --- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs @@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Images return null; }) .Where(i => i is not null) - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .ToList(); } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bf12e4e0a..a3c66dc79 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1175,7 +1175,7 @@ namespace Emby.Server.Implementations.Library } }) .Where(i => i is not null) - .OrderBy(i => i) + .Order() .ToArray(), CollectionType = GetCollectionType(dir) diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 5f1a3ec6d..1522cd3ae 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } return false; - }).OrderBy(i => i).ToList(); + }).Order().ToList(); // If different video types were found, don't allow this if (videoTypes.Distinct().Count() > 1) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 7afc7959c..4003468d0 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -2392,8 +2392,7 @@ namespace Emby.Server.Implementations.LiveTv .Select(i => _libraryManager.FindByPath(i, true)) .Where(i => i is not null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) - .GroupBy(x => x.Id) - .Select(x => x.First()) + .DistinctBy(x => x.Id) .OrderBy(i => i.SortName) .ToList(); diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index ada3c7730..4508363b0 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -12,7 +12,7 @@ "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}", - "Favorites": "مفضلات", + "Favorites": "المفضلة", "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", @@ -91,13 +91,13 @@ "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", "ValueSpecialEpisodeName": "حلقه خاصه - {0}", - "VersionNumber": "النسخة {0}", + "VersionNumber": "الإصدار {0}", "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.", "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة", "TasksChannelsCategory": "قنوات الإنترنت", "TasksLibraryCategory": "مكتبة", "TasksMaintenanceCategory": "صيانة", - "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.", + "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.", "TaskRefreshLibrary": "افحص مكتبة الوسائط", "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.", "TaskRefreshChapterImages": "استخراج صور الفصل", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "تحسين قاعدة البيانات", "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", - "External": "خارجي" + "External": "خارجي", + "HearingImpaired": "ضعاف السمع" } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index c0ed01fdf..1966f6968 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -40,7 +40,7 @@ "Movies": "Pel·lícules", "Music": "Música", "MusicVideos": "Vídeos Musicals", - "NameInstallFailed": "Instal·lació de {0} fallida", + "NameInstallFailed": "{0} instal·lació fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconeguda", "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", @@ -118,7 +118,7 @@ "TaskCleanActivityLog": "Buidar Registre d'Activitat", "Undefined": "Indefinit", "Forced": "Forçat", - "Default": "Defecte", + "Default": "Per defecte", "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", "TaskOptimizeDatabase": "Optimitzar la base de dades", "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index 3a8b89f44..dbbc81eeb 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -108,5 +108,20 @@ "UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია", "UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა", "UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე", - "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა." + "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.", + "TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.", + "NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.", + "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან", + "StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.", + "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა", + "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას", + "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.", + "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.", + "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.", + "TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.", + "TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.", + "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", + "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", + "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", + "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს." } diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index a4b2e75b3..67dcf5b04 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "데이터베이스 최적화", "TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.", "TaskKeyframeExtractor": "키프레임 추출", - "External": "외부" + "External": "외부", + "HearingImpaired": "청각 장애" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index d0b458a8f..d4c15ac87 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność.", "External": "Zewnętrzny", "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.", - "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych" + "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", + "HearingImpaired": "Niedosłyszący" } diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 369a2b0d8..725df98da 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -83,24 +83,6 @@ namespace Emby.Server.Implementations public string YearPath => Path.Combine(InternalMetadataPath, "Year"); /// <summary> - /// Gets the path to the General IBN directory. - /// </summary> - /// <value>The general path.</value> - public string GeneralPath => Path.Combine(InternalMetadataPath, "general"); - - /// <summary> - /// Gets the path to the Ratings IBN directory. - /// </summary> - /// <value>The ratings path.</value> - public string RatingsPath => Path.Combine(InternalMetadataPath, "ratings"); - - /// <summary> - /// Gets the media info images path. - /// </summary> - /// <value>The media info images path.</value> - public string MediaInfoImagesPath => Path.Combine(InternalMetadataPath, "mediainfo"); - - /// <summary> /// Gets the path to the user configuration directory. /// </summary> /// <value>The user configuration directory path.</value> diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index b6780ee20..17d136384 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -92,25 +92,25 @@ namespace Jellyfin.Api.Controllers Years = itemList.Select(i => i.ProductionYear ?? -1) .Where(i => i > 0) .Distinct() - .OrderBy(i => i) + .Order() .ToArray(), Genres = itemList.SelectMany(i => i.Genres) .DistinctNames() - .OrderBy(i => i) + .Order() .ToArray(), Tags = itemList .SelectMany(i => i.Tags) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) + .Order() .ToArray(), OfficialRatings = itemList .Select(i => i.OfficialRating) .Where(i => !string.IsNullOrWhiteSpace(i)) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) + .Order() .ToArray() }; } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs deleted file mode 100644 index c54851b96..000000000 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Linq; -using System.Net.Mime; -using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers -{ - /// <summary> - /// Images By Name Controller. - /// </summary> - [Route("Images")] - public class ImageByNameController : BaseJellyfinApiController - { - private readonly IServerApplicationPaths _applicationPaths; - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="ImageByNameController" /> class. - /// </summary> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param> - public ImageByNameController( - IServerConfigurationManager serverConfigurationManager, - IFileSystem fileSystem) - { - _applicationPaths = serverConfigurationManager.ApplicationPaths; - _fileSystem = fileSystem; - } - - /// <summary> - /// Get all general images. - /// </summary> - /// <response code="200">Retrieved list of images.</response> - /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> - [HttpGet("General")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages() - { - return GetImageList(_applicationPaths.GeneralPath, false); - } - - /// <summary> - /// Get General Image. - /// </summary> - /// <param name="name">The name of the image.</param> - /// <param name="type">Image Type (primary, backdrop, logo, etc).</param> - /// <response code="200">Image stream retrieved.</response> - /// <response code="404">Image not found.</response> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - [HttpGet("General/{name}/{type}")] - [AllowAnonymous] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type) - { - var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) - ? "folder" - : type; - - var path = BaseItem.SupportedImageExtensions - .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i))) - .FirstOrDefault(System.IO.File.Exists); - - if (path is null) - { - return NotFound(); - } - - if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid image path."); - } - - var contentType = MimeTypes.GetMimeType(path); - return File(AsyncFile.OpenRead(path), contentType); - } - - /// <summary> - /// Get all general images. - /// </summary> - /// <response code="200">Retrieved list of images.</response> - /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> - [HttpGet("Ratings")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages() - { - return GetImageList(_applicationPaths.RatingsPath, false); - } - - /// <summary> - /// Get rating image. - /// </summary> - /// <param name="theme">The theme to get the image from.</param> - /// <param name="name">The name of the image.</param> - /// <response code="200">Image stream retrieved.</response> - /// <response code="404">Image not found.</response> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - [HttpGet("Ratings/{theme}/{name}")] - [AllowAnonymous] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public ActionResult GetRatingImage( - [FromRoute, Required] string theme, - [FromRoute, Required] string name) - { - return GetImageFile(_applicationPaths.RatingsPath, theme, name); - } - - /// <summary> - /// Get all media info images. - /// </summary> - /// <response code="200">Image list retrieved.</response> - /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> - [HttpGet("MediaInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages() - { - return GetImageList(_applicationPaths.MediaInfoImagesPath, false); - } - - /// <summary> - /// Get media info image. - /// </summary> - /// <param name="theme">The theme to get the image from.</param> - /// <param name="name">The name of the image.</param> - /// <response code="200">Image stream retrieved.</response> - /// <response code="404">Image not found.</response> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - [HttpGet("MediaInfo/{theme}/{name}")] - [AllowAnonymous] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public ActionResult GetMediaInfoImage( - [FromRoute, Required] string theme, - [FromRoute, Required] string name) - { - return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); - } - - /// <summary> - /// Internal FileHelper. - /// </summary> - /// <param name="basePath">Path to begin search.</param> - /// <param name="theme">Theme to search.</param> - /// <param name="name">File name to search for.</param> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - private ActionResult GetImageFile(string basePath, string theme, string? name) - { - var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme)); - - if (Directory.Exists(themeFolder)) - { - var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) - .FirstOrDefault(System.IO.File.Exists); - - if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) - { - if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid image path."); - } - - var contentType = MimeTypes.GetMimeType(path); - - return PhysicalFile(path, contentType); - } - } - - var allFolder = Path.GetFullPath(Path.Combine(basePath, "all")); - if (Directory.Exists(allFolder)) - { - var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) - .FirstOrDefault(System.IO.File.Exists); - - if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) - { - if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid image path."); - } - - var contentType = MimeTypes.GetMimeType(path); - return PhysicalFile(path, contentType); - } - } - - return NotFound(); - } - - private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes) - { - try - { - return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true) - .Select(i => new ImageByNameInfo - { - Name = _fileSystem.GetFileNameWithoutExtension(i), - FileLength = i.Length, - - // For themeable images, use the Theme property - // For general images, the same object structure is fine, - // but it's not owned by a theme, so call it Context - Theme = supportsThemes ? GetThemeName(i.FullName, path) : null, - Context = supportsThemes ? null : GetThemeName(i.FullName, path), - Format = i.Extension.ToLowerInvariant().TrimStart('.') - }) - .OrderBy(i => i.Name) - .ToList(); - } - catch (IOException) - { - return new List<ImageByNameInfo>(); - } - } - - private string? GetThemeName(string path, string rootImagePath) - { - var parentName = Path.GetDirectoryName(path); - - if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - parentName = Path.GetFileName(parentName); - - return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName; - } - } -} diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index ab2020830..196d509fb 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -770,8 +770,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); result.MetadataReaders = plugins @@ -781,8 +780,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = true }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); result.SubtitleFetchers = plugins @@ -792,8 +790,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = true }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); var typeOptions = new List<LibraryTypeOptionsDto>(); @@ -814,8 +811,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(), ImageFetchers = plugins @@ -826,8 +822,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(), SupportedImageTypes = plugins diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 03f864b4a..3cf079362 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -200,8 +200,7 @@ namespace Jellyfin.Api.Controllers IsMovie = true, EnableGroupByMetadataKey = true, DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Select(x => x.First()) + }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Take(itemLimit) .ToList(); @@ -240,8 +239,7 @@ namespace Jellyfin.Api.Controllers IsMovie = true, EnableGroupByMetadataKey = true, DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Select(x => x.First()) + }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Take(itemLimit) .ToList(); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 420630cdf..a28556476 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,12 +1,5 @@ -using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading; using Jellyfin.Api.Constants; -using Jellyfin.Api.Models.NotificationDtos; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Notifications; @@ -23,41 +16,14 @@ namespace Jellyfin.Api.Controllers public class NotificationsController : BaseJellyfinApiController { private readonly INotificationManager _notificationManager; - private readonly IUserManager _userManager; /// <summary> /// Initializes a new instance of the <see cref="NotificationsController" /> class. /// </summary> /// <param name="notificationManager">The notification manager.</param> - /// <param name="userManager">The user manager.</param> - public NotificationsController(INotificationManager notificationManager, IUserManager userManager) + public NotificationsController(INotificationManager notificationManager) { _notificationManager = notificationManager; - _userManager = userManager; - } - - /// <summary> - /// Gets a user's notifications. - /// </summary> - /// <response code="200">Notifications returned.</response> - /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns> - [HttpGet("{userId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<NotificationResultDto> GetNotifications() - { - return new NotificationResultDto(); - } - - /// <summary> - /// Gets a user's notification summary. - /// </summary> - /// <response code="200">Summary of user's notifications returned.</response> - /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns> - [HttpGet("{userId}/Summary")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<NotificationsSummaryDto> GetNotificationsSummary() - { - return new NotificationsSummaryDto(); } /// <summary> @@ -83,56 +49,5 @@ namespace Jellyfin.Api.Controllers { return _notificationManager.GetNotificationServices(); } - - /// <summary> - /// Sends a notification to all admins. - /// </summary> - /// <param name="notificationDto">The notification request.</param> - /// <response code="204">Notification sent.</response> - /// <returns>A <cref see="NoContentResult"/>.</returns> - [HttpPost("Admin")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto) - { - var notification = new NotificationRequest - { - Name = notificationDto.Name, - Description = notificationDto.Description, - Url = notificationDto.Url, - Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal, - UserIds = _userManager.Users - .Where(user => user.HasPermission(PermissionKind.IsAdministrator)) - .Select(user => user.Id) - .ToArray(), - Date = DateTime.UtcNow, - }; - - _notificationManager.SendNotification(notification, CancellationToken.None); - return NoContent(); - } - - /// <summary> - /// Sets notifications as read. - /// </summary> - /// <response code="204">Notifications set as read.</response> - /// <returns>A <cref see="NoContentResult"/>.</returns> - [HttpPost("{userId}/Read")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRead() - { - return NoContent(); - } - - /// <summary> - /// Sets notifications as unread. - /// </summary> - /// <response code="204">Notifications set as unread.</response> - /// <returns>A <cref see="NoContentResult"/>.</returns> - [HttpPost("{userId}/Unread")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetUnread() - { - return NoContent(); - } } } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 002327d74..568224a42 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -157,7 +157,6 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="userId">The user id.</param> /// <param name="pw">The password as plain text.</param> - /// <param name="password">The password sha1-hash.</param> /// <response code="200">User authenticated.</response> /// <response code="403">Sha1-hashed password only is not allowed.</response> /// <response code="404">User not found.</response> @@ -166,10 +165,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Authenticate with username instead")] public async Task<ActionResult<AuthenticationResult>> AuthenticateUser( [FromRoute, Required] Guid userId, - [FromQuery, Required] string pw, - [FromQuery] string? password) + [FromQuery, Required] string pw) { var user = _userManager.GetUserById(userId); @@ -178,11 +177,6 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) - { - return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed."); - } - AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index e8ce1ca2a..e0245fe4d 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -181,7 +181,7 @@ namespace Jellyfin.Api.Helpers { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); - var options = new VideoOptions + var options = new MediaOptions { MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, @@ -244,8 +244,8 @@ namespace Jellyfin.Api.Helpers // Beginning of Playback Determination var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + ? streamBuilder.GetOptimalAudioStream(options) + : streamBuilder.GetOptimalVideoStream(options); if (streamInfo is not null) { diff --git a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs deleted file mode 100644 index 2c3a6282f..000000000 --- a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// <summary> - /// The admin notification dto. - /// </summary> - public class AdminNotificationDto - { - /// <summary> - /// Gets or sets the notification name. - /// </summary> - public string? Name { get; set; } - - /// <summary> - /// Gets or sets the notification description. - /// </summary> - public string? Description { get; set; } - - /// <summary> - /// Gets or sets the notification level. - /// </summary> - public NotificationLevel? NotificationLevel { get; set; } - - /// <summary> - /// Gets or sets the notification url. - /// </summary> - public string? Url { get; set; } - } -} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs deleted file mode 100644 index af5239ec2..000000000 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// <summary> - /// The notification DTO. - /// </summary> - public class NotificationDto - { - /// <summary> - /// Gets or sets the notification ID. Defaults to an empty string. - /// </summary> - public string Id { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the notification's user ID. Defaults to an empty string. - /// </summary> - public string UserId { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the notification date. - /// </summary> - public DateTime Date { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether the notification has been read. Defaults to false. - /// </summary> - public bool IsRead { get; set; } = false; - - /// <summary> - /// Gets or sets the notification's name. Defaults to an empty string. - /// </summary> - public string Name { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the notification's description. Defaults to an empty string. - /// </summary> - public string Description { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the notification's URL. Defaults to an empty string. - /// </summary> - public string Url { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the notification level. - /// </summary> - public NotificationLevel Level { get; set; } - } -} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs deleted file mode 100644 index 64e92bd83..000000000 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// <summary> - /// A list of notifications with the total record count for pagination. - /// </summary> - public class NotificationResultDto - { - /// <summary> - /// Gets or sets the current page of notifications. - /// </summary> - public IReadOnlyList<NotificationDto> Notifications { get; set; } = Array.Empty<NotificationDto>(); - - /// <summary> - /// Gets or sets the total number of notifications. - /// </summary> - public int TotalRecordCount { get; set; } - } -} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs deleted file mode 100644 index 0568dea66..000000000 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// <summary> - /// The notification summary DTO. - /// </summary> - public class NotificationsSummaryDto - { - /// <summary> - /// Gets or sets the number of unread notifications. - /// </summary> - public int UnreadCount { get; set; } - - /// <summary> - /// Gets or sets the maximum unread notification level. - /// </summary> - public NotificationLevel? MaxUnreadNotificationLevel { get; set; } - } -} diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs index 41f7b169e..31208264f 100644 --- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs @@ -1,6 +1,4 @@ -using System; - -namespace Jellyfin.Api.Models.UserDtos +namespace Jellyfin.Api.Models.UserDtos { /// <summary> /// The authenticate user by name request body. @@ -16,11 +14,5 @@ namespace Jellyfin.Api.Models.UserDtos /// Gets or sets the plain text password. /// </summary> public string? Pw { get; set; } - - /// <summary> - /// Gets or sets the sha1-hashed password. - /// </summary> - [Obsolete("Send password using pw field")] - public string? Password { get; set; } } } diff --git a/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs new file mode 100644 index 000000000..bf00dcd53 --- /dev/null +++ b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Jellyfin.Server.HealthChecks; + +/// <summary> +/// Implementation of the <see cref="DbContextHealthCheck{TContext}"/> for a <see cref="IDbContextFactory{TContext}"/>. +/// </summary> +/// <typeparam name="TContext">The type of database context.</typeparam> +public class DbContextFactoryHealthCheck<TContext> : IHealthCheck + where TContext : DbContext +{ + private readonly IDbContextFactory<TContext> _dbContextFactory; + + /// <summary> + /// Initializes a new instance of the <see cref="DbContextFactoryHealthCheck{TContext}"/> class. + /// </summary> + /// <param name="contextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param> + public DbContextFactoryHealthCheck(IDbContextFactory<TContext> contextFactory) + { + _dbContextFactory = contextFactory; + } + + /// <inheritdoc /> + public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + + var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + if (await dbContext.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false)) + { + return HealthCheckResult.Healthy(); + } + } + + return HealthCheckResult.Unhealthy(); + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 49a57aa68..5d6a278c4 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -8,6 +8,7 @@ using System.Text; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; +using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; @@ -122,7 +123,7 @@ namespace Jellyfin.Server .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHealthChecks() - .AddDbContextCheck<JellyfinDb>(); + .AddCheck<DbContextFactoryHealthCheck<JellyfinDb>>(nameof(JellyfinDb)); services.AddHlsPlaylistGenerator(); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 49dd151f3..f2c2007f7 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -56,6 +56,7 @@ namespace MediaBrowser.Controller.Entities ".srt", ".vtt", ".sub", + ".sup", ".idx", ".txt", ".edl", diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 7dc7f774d..5ac619d8f 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -355,8 +355,7 @@ namespace MediaBrowser.Controller.Entities return PhysicalLocations .Where(i => !FileSystem.AreEqual(i, Path)) .SelectMany(i => GetPhysicalParents(i, rootChildren)) - .GroupBy(x => x.Id) - .Select(x => x.First()); + .DistinctBy(x => x.Id); } private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 02312757c..e7a8a773e 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -283,7 +283,7 @@ namespace MediaBrowser.Controller.Entities.TV // This depends on settings for that series // When this happens, remove the duplicate from season 0 - return allEpisodes.GroupBy(i => i.Id).Select(x => x.First()).Reverse(); + return allEpisodes.DistinctBy(i => i.Id).Reverse(); } public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 1890dbb36..608286cd8 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -51,24 +51,6 @@ namespace MediaBrowser.Controller string YearPath { get; } /// <summary> - /// Gets the path to the General IBN directory. - /// </summary> - /// <value>The general path.</value> - string GeneralPath { get; } - - /// <summary> - /// Gets the path to the Ratings IBN directory. - /// </summary> - /// <value>The ratings path.</value> - string RatingsPath { get; } - - /// <summary> - /// Gets the media info images path. - /// </summary> - /// <value>The media info images path.</value> - string MediaInfoImagesPath { get; } - - /// <summary> /// Gets the path to the user configuration directory. /// </summary> /// <value>The user configuration directory path.</value> diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs index 919570e89..ee37fb2dc 100644 --- a/MediaBrowser.Controller/Library/NameExtensions.cs +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -10,8 +10,7 @@ namespace MediaBrowser.Controller.Library public static class NameExtensions { public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) - => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()); + => names.DistinctBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase); private static string RemoveDiacritics(string? name) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 7264c5eed..b40c224d5 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -938,8 +938,10 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleStream.IsExternal) { var subtitlePath = state.SubtitleStream.Path; + var subtitleExtension = Path.GetExtension(subtitlePath); - if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); if (File.Exists(idxFile)) diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 375041490..7404c2868 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -27,7 +27,7 @@ <ItemGroup> <PackageReference Include="BDInfo" Version="0.7.6.2" /> - <PackageReference Include="libse" Version="3.6.5" /> + <PackageReference Include="libse" Version="3.6.10" /> <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" /> <PackageReference Include="UTF.Unknown" Version="2.5.1" /> diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 7a3462b97..18e248a1b 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -144,7 +144,8 @@ namespace MediaBrowser.MediaEncoding.Probing FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ?? - FFProbeHelpers.GetDictionaryDateTime(tags, "date"); + FFProbeHelpers.GetDictionaryDateTime(tags, "date") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "creation_time"); // Set common metadata for music (audio) and music videos (video) info.Album = tags.GetValueOrDefault("album"); diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index df4018fdd..29aecf97f 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using MediaBrowser.Model.Dto; @@ -7,11 +6,14 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Dlna { /// <summary> - /// Class AudioOptions. + /// Class MediaOptions. /// </summary> - public class AudioOptions + public class MediaOptions { - public AudioOptions() + /// <summary> + /// Initializes a new instance of the <see cref="MediaOptions"/> class. + /// </summary> + public MediaOptions() { Context = EncodingContext.Streaming; @@ -19,20 +21,49 @@ namespace MediaBrowser.Model.Dlna EnableDirectStream = true; } + /// <summary> + /// Gets or sets a value indicating whether direct playback is allowed. + /// </summary> public bool EnableDirectPlay { get; set; } + /// <summary> + /// Gets or sets a value indicating whether direct streaming is allowed. + /// </summary> public bool EnableDirectStream { get; set; } + /// <summary> + /// Gets or sets a value indicating whether direct playback is forced. + /// </summary> public bool ForceDirectPlay { get; set; } + /// <summary> + /// Gets or sets a value indicating whether direct streaming is forced. + /// </summary> public bool ForceDirectStream { get; set; } + /// <summary> + /// Gets or sets a value indicating whether audio stream copy is allowed. + /// </summary> public bool AllowAudioStreamCopy { get; set; } + /// <summary> + /// Gets or sets a value indicating whether video stream copy is allowed. + /// </summary> + public bool AllowVideoStreamCopy { get; set; } + + /// <summary> + /// Gets or sets the item id. + /// </summary> public Guid ItemId { get; set; } + /// <summary> + /// Gets or sets the media sources. + /// </summary> public MediaSourceInfo[] MediaSources { get; set; } + /// <summary> + /// Gets or sets the device profile. + /// </summary> public DeviceProfile Profile { get; set; } /// <summary> @@ -40,6 +71,9 @@ namespace MediaBrowser.Model.Dlna /// </summary> public string MediaSourceId { get; set; } + /// <summary> + /// Gets or sets the device id. + /// </summary> public string DeviceId { get; set; } /// <summary> @@ -49,7 +83,7 @@ namespace MediaBrowser.Model.Dlna public int? MaxAudioChannels { get; set; } /// <summary> - /// Gets or sets the application's configured quality setting. + /// Gets or sets the application's configured maximum bitrate. /// </summary> public int? MaxBitrate { get; set; } @@ -66,6 +100,16 @@ namespace MediaBrowser.Model.Dlna public int? AudioTranscodingBitrate { get; set; } /// <summary> + /// Gets or sets an override for the audio stream index. + /// </summary> + public int? AudioStreamIndex { get; set; } + + /// <summary> + /// Gets or sets an override for the subtitle stream index. + /// </summary> + public int? SubtitleStreamIndex { get; set; } + + /// <summary> /// Gets the maximum bitrate. /// </summary> /// <param name="isAudio">Whether or not this is audio.</param> diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 4c964c21a..bb41c9979 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Model.Dlna { + /// <summary> + /// Class StreamBuilder. + /// </summary> public class StreamBuilder { // Aliases @@ -24,42 +26,56 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; + /// <summary> + /// Initializes a new instance of the <see cref="StreamBuilder"/> class. + /// </summary> + /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/> object.</param> + /// <param name="logger">The <see cref="ILogger"/> object.</param> public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger) { _transcoderSupport = transcoderSupport; _logger = logger; } + /// <summary> + /// Initializes a new instance of the <see cref="StreamBuilder"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger"/> object.</param> public StreamBuilder(ILogger<StreamBuilder> logger) : this(new FullTranscoderSupport(), logger) { } - public StreamInfo BuildAudioItem(AudioOptions options) + /// <summary> + /// Gets the optimal audio stream. + /// </summary> + /// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param> + /// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns> + public StreamInfo GetOptimalAudioStream(MediaOptions options) { - ValidateAudioInput(options); + ValidateMediaOptions(options, false); var mediaSources = new List<MediaSourceInfo>(); - foreach (MediaSourceInfo i in options.MediaSources) + foreach (var mediaSource in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { - mediaSources.Add(i); + mediaSources.Add(mediaSource); } } var streams = new List<StreamInfo>(); - foreach (MediaSourceInfo i in mediaSources) + foreach (var mediaSourceInfo in mediaSources) { - StreamInfo streamInfo = BuildAudioItem(i, options); + StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options); if (streamInfo is not null) { streams.Add(streamInfo); } } - foreach (StreamInfo stream in streams) + foreach (var stream in streams) { stream.DeviceId = options.DeviceId; stream.DeviceProfileId = options.Profile.Id; @@ -68,31 +84,137 @@ namespace MediaBrowser.Model.Dlna return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); } - public StreamInfo BuildVideoItem(VideoOptions options) + private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options) { - ValidateInput(options); + var playlistItem = new StreamInfo + { + ItemId = options.ItemId, + MediaType = DlnaProfileType.Audio, + MediaSource = item, + RunTimeTicks = item.RunTimeTicks, + Context = options.Context, + DeviceProfile = options.Profile + }; + + if (options.ForceDirectPlay) + { + playlistItem.PlayMethod = PlayMethod.DirectPlay; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + if (options.ForceDirectStream) + { + playlistItem.PlayMethod = PlayMethod.DirectStream; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + MediaStream audioStream = item.GetDefaultAudioStream(null); + + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); + + var directPlayMethod = directPlayInfo.PlayMethod; + var transcodeReasons = directPlayInfo.TranscodeReasons; + + var inputAudioChannels = audioStream?.Channels; + var inputAudioBitrate = audioStream?.BitDepth; + var inputAudioSampleRate = audioStream?.SampleRate; + var inputAudioBitDepth = audioStream?.BitDepth; + + if (directPlayMethod.HasValue) + { + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); + var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); + transcodeReasons |= audioFailureReasons; + + if (audioFailureReasons == 0) + { + playlistItem.PlayMethod = directPlayMethod.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); + + return playlistItem; + } + } + + TranscodingProfile transcodingProfile = null; + foreach (var tcProfile in options.Profile.TranscodingProfiles) + { + if (tcProfile.Type == playlistItem.MediaType + && tcProfile.Context == options.Context + && _transcoderSupport.CanEncodeToAudioCodec(transcodingProfile.AudioCodec ?? tcProfile.Container)) + { + transcodingProfile = tcProfile; + break; + } + } + + if (transcodingProfile != null) + { + if (!item.SupportsTranscoding) + { + return null; + } + + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + + var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); + ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); + + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + + var configuredBitrate = options.GetMaxBitrate(true); + + long transcodingBitrate = options.AudioTranscodingBitrate + ?? (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) + ?? configuredBitrate + ?? 128000; + + if (configuredBitrate.HasValue) + { + transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); + } + + var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); + playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + } + + playlistItem.TranscodeReasons = transcodeReasons; + return playlistItem; + } + + /// <summary> + /// Gets the optimal video stream. + /// </summary> + /// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param> + /// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns> + public StreamInfo GetOptimalVideoStream(MediaOptions options) + { + ValidateMediaOptions(options, true); var mediaSources = new List<MediaSourceInfo>(); - foreach (MediaSourceInfo i in options.MediaSources) + foreach (var mediaSourceInfo in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { - mediaSources.Add(i); + mediaSources.Add(mediaSourceInfo); } } var streams = new List<StreamInfo>(); - foreach (MediaSourceInfo i in mediaSources) + foreach (var mediaSourceInfo in mediaSources) { - var streamInfo = BuildVideoItem(i, options); + var streamInfo = BuildVideoItem(mediaSourceInfo, options); if (streamInfo is not null) { streams.Add(streamInfo); } } - foreach (StreamInfo stream in streams) + foreach (var stream in streams) { stream.DeviceId = options.DeviceId; stream.DeviceProfileId = options.Profile.Id; @@ -236,6 +358,14 @@ namespace MediaBrowser.Model.Dlna } } + /// <summary> + /// Normalizes input container. + /// </summary> + /// <param name="inputContainer">The input container.</param> + /// <param name="profile">The <see cref="DeviceProfile"/>.</param> + /// <param name="type">The <see cref="DlnaProfileType"/>.</param> + /// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param> + /// <returns>The the normalized input container.</returns> public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) @@ -264,108 +394,7 @@ namespace MediaBrowser.Model.Dlna return formats[0]; } - private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options) - { - StreamInfo playlistItem = new StreamInfo - { - ItemId = options.ItemId, - MediaType = DlnaProfileType.Audio, - MediaSource = item, - RunTimeTicks = item.RunTimeTicks, - Context = options.Context, - DeviceProfile = options.Profile - }; - - if (options.ForceDirectPlay) - { - playlistItem.PlayMethod = PlayMethod.DirectPlay; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); - return playlistItem; - } - - if (options.ForceDirectStream) - { - playlistItem.PlayMethod = PlayMethod.DirectStream; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); - return playlistItem; - } - - var audioStream = item.GetDefaultAudioStream(null); - - var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); - - var directPlayMethod = directPlayInfo.PlayMethod; - var transcodeReasons = directPlayInfo.TranscodeReasons; - - int? inputAudioChannels = audioStream?.Channels; - int? inputAudioBitrate = audioStream?.BitDepth; - int? inputAudioSampleRate = audioStream?.SampleRate; - int? inputAudioBitDepth = audioStream?.BitDepth; - - if (directPlayMethod.HasValue) - { - var profile = options.Profile; - var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); - var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); - transcodeReasons |= audioFailureReasons; - - if (audioFailureReasons == 0) - { - playlistItem.PlayMethod = directPlayMethod.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); - - return playlistItem; - } - } - - TranscodingProfile transcodingProfile = null; - foreach (var i in options.Profile.TranscodingProfiles) - { - if (i.Type == playlistItem.MediaType - && i.Context == options.Context - && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) - { - transcodingProfile = i; - break; - } - } - - if (transcodingProfile is not null) - { - if (!item.SupportsTranscoding) - { - return null; - } - - SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); - - var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); - ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); - - // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - - var configuredBitrate = options.GetMaxBitrate(true); - - long transcodingBitrate = options.AudioTranscodingBitrate ?? - (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? - configuredBitrate ?? - 128000; - - if (configuredBitrate.HasValue) - { - transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); - } - - var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); - playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); - } - - playlistItem.TranscodeReasons = transcodeReasons; - return playlistItem; - } - - private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) { var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); @@ -388,7 +417,7 @@ namespace MediaBrowser.Model.Dlna // If device requirements are satisfied then allow both direct stream and direct play if (item.SupportsDirectPlay) { - if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay)) + if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0)) { if (options.EnableDirectPlay) { @@ -404,7 +433,7 @@ namespace MediaBrowser.Model.Dlna // While options takes the network and other factors into account. Only applies to direct stream if (item.SupportsDirectStream) { - if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0)) { if (options.EnableDirectStream) { @@ -427,7 +456,6 @@ namespace MediaBrowser.Model.Dlna var containerSupported = false; var audioSupported = false; var videoSupported = false; - TranscodeReason reasons = 0; foreach (var profile in directPlayProfiles) { @@ -447,6 +475,7 @@ namespace MediaBrowser.Model.Dlna } } + TranscodeReason reasons = 0; if (!containerSupported) { reasons |= TranscodeReason.ContainerNotSupported; @@ -547,7 +576,7 @@ namespace MediaBrowser.Model.Dlna } } - private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) { var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); var protocol = "http"; @@ -562,7 +591,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); } - private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) + private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) { ArgumentNullException.ThrowIfNull(item); @@ -601,11 +630,15 @@ namespace MediaBrowser.Model.Dlna var videoStream = item.VideoStream; - var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); - var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); - bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0); - bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0); - var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility; + var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0); + var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded); + var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded); + TranscodeReason transcodeReasons = 0; + + if (bitrateLimitExceeded) + { + transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit; + } _logger.LogDebug( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", @@ -702,7 +735,7 @@ namespace MediaBrowser.Model.Dlna } } - _logger.LogInformation( + _logger.LogDebug( "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", options.Profile.Name ?? "Anonymous Profile", item.Path ?? "Unknown path", @@ -716,7 +749,7 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { @@ -763,7 +796,7 @@ namespace MediaBrowser.Model.Dlna return transcodingProfiles.FirstOrDefault(); } - private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec) + private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec) { // Prefer matching video codecs var videoCodecs = ContainerProfile.SplitValue(videoCodec); @@ -867,7 +900,7 @@ namespace MediaBrowser.Model.Dlna // Honor requested max channels playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream); @@ -882,14 +915,14 @@ namespace MediaBrowser.Model.Dlna i.ContainsAnyCodec(audioCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); isFirstAppliedCodecProfile = true; - foreach (var i in appliedAudioConditions) + foreach (var codecProfile in appliedAudioConditions) { var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); foreach (var transcodingAudioCodec in transcodingAudioCodecs) { - if (i.ContainsAnyCodec(transcodingAudioCodec, container)) + if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); + ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); isFirstAppliedCodecProfile = false; break; } @@ -1050,7 +1083,7 @@ namespace MediaBrowser.Model.Dlna } private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( - VideoOptions options, + MediaOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, @@ -1237,7 +1270,7 @@ namespace MediaBrowser.Model.Dlna return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); } - private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) + private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) { var profile = options.Profile; var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); @@ -1274,23 +1307,17 @@ namespace MediaBrowser.Model.Dlna mediaSource.Path ?? "Unknown path"); } - private TranscodeReason IsBitrateEligibleForDirectPlayback( - MediaSourceInfo item, - long maxBitrate, - VideoOptions options, - PlayMethod playMethod) - { - bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod); - if (!result) - { - return TranscodeReason.ContainerBitrateExceedsLimit; - } - else - { - return 0; - } - } - + /// <summary> + /// Normalizes input container. + /// </summary> + /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param> + /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param> + /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param> + /// <param name="playMethod">The <see cref="PlayMethod"/>.</param> + /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param> + /// <param name="outputContainer">The output container.</param> + /// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param> + /// <returns>The the normalized input container.</returns> public static SubtitleProfile GetSubtitleProfile( MediaSourceInfo mediaSource, MediaStream subtitleStream, @@ -1448,12 +1475,12 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate) { // Don't restrict bitrate if item is remote. if (item.IsRemote) { - return true; + return false; } // If no maximum bitrate is set, default to no maximum bitrate. @@ -1465,40 +1492,22 @@ namespace MediaBrowser.Model.Dlna if (itemBitrate > requestedMaxBitrate) { _logger.LogDebug( - "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", - playMethod, + "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", itemBitrate, requestedMaxBitrate); - return false; - } - - return true; - } - - private static void ValidateInput(VideoOptions options) - { - ValidateAudioInput(options); - - if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); + return true; } - if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); - } + return false; } - private static void ValidateAudioInput(AudioOptions options) + private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource) { if (options.ItemId.Equals(default)) { - throw new ArgumentException("ItemId is required"); + ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); } - ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); - if (options.Profile is null) { throw new ArgumentException("Profile is required"); @@ -1508,6 +1517,19 @@ namespace MediaBrowser.Model.Dlna { throw new ArgumentException("MediaSources is required"); } + + if (isMediaSource) + { + if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); + } + + if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); + } + } } private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio( @@ -1825,8 +1847,8 @@ namespace MediaBrowser.Model.Dlna continue; } - // change from split by | to comma - // strip spaces to avoid having to encode + // Change from split by | to comma + // Strip spaces to avoid having to encode var values = value .Split('|', StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs deleted file mode 100644 index 0cb80af54..000000000 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - /// <summary> - /// Class VideoOptions. - /// </summary> - public class VideoOptions : AudioOptions - { - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public bool AllowVideoStreamCopy { get; set; } - } -} diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 344ebaf80..47341f4e1 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -635,11 +635,12 @@ namespace MediaBrowser.Model.Entities // sub = external .sub file - return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) && - !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) && - !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) && - !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) && - !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); + return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) + && !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) + && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) + && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) + && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase) + && !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); } public bool SupportsSubtitleConversionTo(string toCodec) diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index a5a6b18aa..c6d1f3900 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -24,24 +24,27 @@ namespace MediaBrowser.Model.Extensions requestedLanguage = "en"; } - var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase); - return remoteImageInfos.OrderByDescending(i => { + // Image priority ordering: + // - Images that match the requested language + // - Images with no language + // - TODO: Images that match the original language + // - Images in English + // - Images that don't match the requested language + if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) { - return 3; + return 4; } if (string.IsNullOrEmpty(i.Language)) { - // Assume empty image language is likely to be English. - return isRequestedLanguageEn ? 3 : 2; + return 3; } - if (!isRequestedLanguageEn && string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) { - // Prioritize English over non-requested languages. return 2; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 8e5d16baf..ffae77200 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -444,8 +444,8 @@ namespace MediaBrowser.Providers.Manager } } - if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) || - (originalProductionYear ?? -1) != (item.ProductionYear ?? -1)) + if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) + || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1)) { updateType |= ItemUpdateType.MetadataEdit; } @@ -465,7 +465,7 @@ namespace MediaBrowser.Providers.Manager .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - if (currentList.Length != item.Genres.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Genres.OrderBy(i => i), StringComparer.OrdinalIgnoreCase)) + if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), StringComparer.OrdinalIgnoreCase)) { updateType |= ItemUpdateType.MetadataEdit; } @@ -486,7 +486,7 @@ namespace MediaBrowser.Providers.Manager .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - if (currentList.Length != item.Studios.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Studios.OrderBy(i => i), StringComparer.OrdinalIgnoreCase)) + if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order(), StringComparer.OrdinalIgnoreCase)) { updateType |= ItemUpdateType.MetadataEdit; } @@ -47,16 +47,16 @@ Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! -For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html). +For further details, please see [our documentation page](https://jellyfin.org/docs/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://jellyfin.org/docs/general/getting-help). For more information about the project, please see our [about page](https://jellyfin.org/docs/general/about). <strong>Want to get started?</strong><br/> -Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://docs.jellyfin.org/general/administration/installing.html">installation guide</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>. You can also <a href="https://docs.jellyfin.org/general/administration/building.html">build from source</a>.<br/> +Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://jellyfin.org/docs/general/installation/">installation guide</a>, then see our <a href="https://jellyfin.org/docs/general/quick-start">quick start guide</a>. You can also <a href="https://jellyfin.org/docs/general/installation/source">build from source</a>.<br/> <strong>Something not working right?</strong><br/> -Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a> on GitHub.<br/> +Open an <a href="https://jellyfin.org/docs/general/contributing/issues">Issue</a> on GitHub.<br/> <strong>Want to contribute?</strong><br/> -Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://docs.jellyfin.org/general/contributing/index.html">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/> +Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://jellyfin.org/docs/general/contributing/">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/> <strong>New idea or improvement?</strong><br/> Check out our <a href="https://features.jellyfin.org/?view=most-wanted">feature request hub</a>.<br/> diff --git a/debian/postinst b/debian/postinst index 2f9c4cffb..47173855f 100644 --- a/debian/postinst +++ b/debian/postinst @@ -83,7 +83,7 @@ fi # End automatically added section # Automatically added by dh_installinit if [[ "$1" == "configure" ]] || [[ "$1" == "abort-upgrade" ]]; then - if [[ -d "/run/systemd/systemd" ]]; then + if [[ -d "/run/systemd/system" ]]; then systemctl --system daemon-reload >/dev/null || true deb-systemd-invoke start jellyfin >/dev/null || true elif [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 6cc814ef4..9c2449da4 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -18,7 +18,7 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="Moq" Version="4.18.3" /> + <PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="SharpFuzz" Version="2.0.0" /> </ItemGroup> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index eaf2bc35c..9fed8cbd9 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -27,6 +27,11 @@ <Compile Include="../../SharedVersion.cs" /> </ItemGroup> + + <ItemGroup> + <PackageReference Include="Diacritics" Version="3.3.14" /> + </ItemGroup> + <!-- Code Analyzers--> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index b19be071b..f30b63945 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -20,23 +20,8 @@ namespace Jellyfin.Extensions /// <param name="text">The string to act on.</param> /// <returns>The string without diacritics character.</returns> public static string RemoveDiacritics(this string text) - { - string withDiactritics = _nonConformingUnicode - .Replace(text, string.Empty) - .Normalize(NormalizationForm.FormD); - - var withoutDiactritics = new StringBuilder(); - foreach (char c in withDiactritics) - { - UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(c); - if (uc != UnicodeCategory.NonSpacingMark) - { - withoutDiactritics.Append(c); - } - } - - return withoutDiactritics.ToString().Normalize(NormalizationForm.FormC); - } + => Diacritics.Extensions.StringExtensions.RemoveDiacritics( + _nonConformingUnicode.Replace(text, string.Empty)); /// <summary> /// Checks whether or not the specified string has diacritics in it. @@ -44,9 +29,8 @@ namespace Jellyfin.Extensions /// <param name="text">The string to check.</param> /// <returns>True if the string has diacritics, false otherwise.</returns> public static bool HasDiacritics(this string text) - { - return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal); - } + => Diacritics.Extensions.StringExtensions.HasDiacritics(text) + || _nonConformingUnicode.IsMatch(text); /// <summary> /// Counts the number of occurrences of [needle] in the string. diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs index 7730841a1..2a7e8fafd 100644 --- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs +++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Extensions.Tests { var copy = strings.Reverse().ToArray(); Array.Sort(copy, new AlphanumericComparator()); - Assert.True(strings.SequenceEqual(copy)); + Assert.Equal(strings, copy); } } } diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs index 903d88caa..69d20bd3f 100644 --- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs +++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs @@ -9,12 +9,15 @@ namespace Jellyfin.Extensions.Tests [InlineData("", "")] // Identity edge-case (no diactritics) [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics) [InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping + [InlineData("åäö", "aao")] // Issue #7484 [InlineData("Jön", "Jon")] // Issue #7484 [InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484 [InlineData("Kieślowski", "Kieslowski")] // Issue #7450 [InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560 [InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support) [InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393 + [InlineData("Le cœur a ses raisons", "Le coeur a ses raisons")] // Issue #8893 + [InlineData("Béla Tarr", "Bela Tarr")] // Issue #8893 public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult) { string result = input.RemoveDiacritics(); @@ -25,12 +28,15 @@ namespace Jellyfin.Extensions.Tests [InlineData("", false)] // Identity edge-case (no diactritics) [InlineData("Indiana Jones", false)] // Identity (no diactritics) [InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping + [InlineData("åäö", true)] // Issue #7484 [InlineData("Jön", true)] // Issue #7484 [InlineData("Jönssonligan", true)] // Issue #7484 [InlineData("Kieślowski", true)] // Issue #7450 [InlineData("Cidadão Kane", true)] // Issue #7560 [InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support) [InlineData("애타는 로맨스", false)] // Issue #6393 + [InlineData("Le cœur a ses raisons", true)] // Issue #8893 + [InlineData("Béla Tarr", true)] // Issue #8893 public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult) { bool result = input.HasDiacritics(); diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 5e11a7232..60be17a74 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -164,7 +164,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); } @@ -262,7 +262,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); options.AudioStreamIndex = 1; options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; @@ -298,7 +298,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); var streamCount = options.MediaSources[0].MediaStreams.Count; if (streamCount > 0) { @@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); } - private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) { if (string.IsNullOrEmpty(transcodeProtocol)) { @@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests var builder = GetStreamBuilder(); - var val = builder.BuildVideoItem(options); - Assert.NotNull(val); + var streamInfo = builder.GetOptimalVideoStream(options); + Assert.NotNull(streamInfo); if (playMethod is not null) { - Assert.Equal(playMethod, val.PlayMethod); + Assert.Equal(playMethod, streamInfo.PlayMethod); } - Assert.Equal(why, val.TranscodeReasons); + Assert.Equal(why, streamInfo.TranscodeReasons); var audioStreamIndexInput = options.AudioStreamIndex; - var targetVideoStream = val.TargetVideoStream; - var targetAudioStream = val.TargetAudioStream; + var targetVideoStream = streamInfo.TargetVideoStream; + var targetAudioStream = streamInfo.TargetAudioStream; - var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); + var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId); Assert.NotNull(mediaSource); var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); // TODO: Check AudioStreamIndex vs options.AudioStreamIndex var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); - var uri = ParseUri(val); + var uri = ParseUri(streamInfo); if (playMethod == PlayMethod.DirectPlay) { @@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); - Assert.Single(val.TargetAudioCodec); + Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec); + Assert.Single(streamInfo.TargetAudioCodec); // Assert.Single(val.AudioCodecs); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); } } else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) { - Assert.NotNull(val.Container); - Assert.NotEmpty(val.VideoCodecs); - Assert.NotEmpty(val.AudioCodecs); + Assert.NotNull(streamInfo.Container); + Assert.NotEmpty(streamInfo.VideoCodecs); + Assert.NotEmpty(streamInfo.AudioCodecs); // Check expected container (todo: this could be a test param) if (transcodeProtocol.Equals("http", StringComparison.Ordinal)) { // Assert.Equal("webm", val.Container); - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); Assert.Equal("stream", uri.Filename); - Assert.Equal("http", val.SubProtocol); + Assert.Equal("http", streamInfo.SubProtocol); } else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal)) { - Assert.Equal("mp4", val.Container); + Assert.Equal("mp4", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } else { - Assert.Equal("ts", val.Container); + Assert.Equal("ts", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } // Full transcode if (transcodeMode.Equals("Transcode", StringComparison.Ordinal)) { - if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) { Assert.All( videoStreams, - stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs)); } - // TODO: Fill out tests here + // TODO: fill out tests here } // DirectStream and Remux else { // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { // Check expected audio codecs (1) if (!targetAudioStream.IsExternal) { - if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) + // Check expected audio codecs (1) + if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) { - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); } else { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } } else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) { // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); - Assert.Single(val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); + Assert.Single(streamInfo.AudioCodecs); } // Video details var videoStream = targetVideoStream; - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); - Assert.Equal(videoStream.Level, val.TargetVideoLevel); - Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); - Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); + Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); + Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel); + Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth); + Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); // Audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != 0) @@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests // TODO:fixme if (!targetAudioStream.IsExternal) { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } @@ -465,7 +466,7 @@ namespace Jellyfin.Model.Tests { if (!stream.IsExternal) { - Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs); } }); } @@ -474,14 +475,14 @@ namespace Jellyfin.Model.Tests } else if (playMethod is null) { - Assert.Null(val.SubProtocol); + Assert.Null(streamInfo.SubProtocol); Assert.Equal("stream", uri.Filename); - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); } - return val; + return streamInfo; } private static async ValueTask<T> TestData<T>(string name) @@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests return new StreamBuilder(transcodeSupport.Object, logger); } - private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources) + private static async ValueTask<MediaOptions> GetMediaOptions(string deviceProfile, params string[] sources) { var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src)) .Select(val => val.Result) @@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests var dp = await TestData<DeviceProfile>(deviceProfile); - return new VideoOptions() + return new MediaOptions() { ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), MediaSourceId = mediaSourceId, diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index 1574bce58..6c9c98cbe 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -10,6 +10,7 @@ namespace Jellyfin.Naming.Tests.Video [Theory] [InlineData("Super movie 480p.mp4", "Super movie")] + [InlineData("Super movie Multi.mp4", "Super movie")] [InlineData("Super movie 480p 2001.mp4", "Super movie")] [InlineData("Super movie [480p].mp4", "Super movie")] [InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")] |
