diff options
60 files changed, 382 insertions, 340 deletions
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 1db50f0d3..bdbfcd3eb 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -105,7 +105,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.0 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} @@ -141,7 +141,7 @@ jobs: publish: name: OpenAPI - Publish Unstable Spec if: | - github.event_name != 'pull_request' && + github.event_name != 'pull_request_target' && contains(github.repository_owner, 'jellyfin') runs-on: ubuntu-latest needs: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 386f8d321..5055bbfa5 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -121,3 +121,28 @@ jobs: ${{ steps.run_tests.outputs.output }} reactions: confused + + rename: + name: Rename + if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' + runs-on: ubuntu-latest + steps: + - name: pull in script + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + repository: jellyfin/jellyfin-triage-script + - name: install python + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: '3.12' + cache: 'pip' + - name: install python packages + run: pip install -r rename/requirements.txt + - name: run rename script + run: python3 rename.py + working-directory: ./rename + env: + GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }} + GH_REPO: ${{ github.repository }} + ISSUE: ${{ github.event.issue.number }} + COMMENT_ID: ${{ github.event.comment.id }} diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index b553db6e2..e53234641 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -14,7 +14,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.12' cache: 'pip' diff --git a/Directory.Packages.props b/Directory.Packages.props index 1f853ef73..9367d397d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ <PackageVersion Include="Diacritics" Version="3.3.27" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.3" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.3.1" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" /> <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" /> @@ -73,7 +73,7 @@ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> - <PackageVersion Include="Svg.Skia" Version="1.0.0.16" /> + <PackageVersion Include="Svg.Skia" Version="1.0.0.18" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 4bd226d95..333d237a2 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -540,6 +540,12 @@ namespace Emby.Naming.Common new ExtraRule( ExtraType.Unknown, ExtraRuleType.DirectoryName, + "extra", + MediaType.Video), + + new ExtraRule( + ExtraType.Unknown, + ExtraRuleType.DirectoryName, "other", MediaType.Video), diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b34d0f21e..e414792ba 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Collections var name = _localizationManager.GetLocalizedString("Collections"); - await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false); + await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false); return FindFolders(path).First(); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index a6336f145..59e4ff1a9 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -205,7 +205,7 @@ namespace Emby.Server.Implementations.Data private static readonly string _mediaAttachmentSaveColumnsSelectQuery = $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId"; - private static readonly string _mediaAttachmentInsertPrefix; + private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix(); private static readonly BaseItemKind[] _programTypes = new[] { @@ -296,21 +296,6 @@ namespace Emby.Server.Implementations.Data { BaseItemKind.Year, typeof(Year).FullName } }; - static SqliteItemRepository() - { - var queryPrefixText = new StringBuilder(); - queryPrefixText.Append("insert into mediaattachments ("); - foreach (var column in _mediaAttachmentSaveColumns) - { - queryPrefixText.Append(column) - .Append(','); - } - - queryPrefixText.Length -= 1; - queryPrefixText.Append(") values "); - _mediaAttachmentInsertPrefix = queryPrefixText.ToString(); - } - /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> @@ -5879,6 +5864,21 @@ AND Type = @InternalPersonType)"); return item; } + private static string BuildMediaAttachmentInsertPrefix() + { + var queryPrefixText = new StringBuilder(); + queryPrefixText.Append("insert into mediaattachments ("); + foreach (var column in _mediaAttachmentSaveColumns) + { + queryPrefixText.Append(column) + .Append(','); + } + + queryPrefixText.Length -= 1; + queryPrefixText.Append(") values "); + return queryPrefixText.ToString(); + } + #nullable enable private readonly struct QueryTimeLogger : IDisposable diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7812687ea..5da9bea26 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -903,10 +903,7 @@ namespace Emby.Server.Implementations.Dto if (item is Audio audio) { dto.Album = audio.Album; - if (audio.ExtraType.HasValue) - { - dto.ExtraType = audio.ExtraType.Value.ToString(); - } + dto.ExtraType = audio.ExtraType; var albumParent = audio.AlbumEntity; @@ -1058,10 +1055,7 @@ namespace Emby.Server.Implementations.Dto dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); } - if (video.ExtraType.HasValue) - { - dto.ExtraType = video.ExtraType.Value.ToString(); - } + dto.ExtraType = video.ExtraType; } if (options.ContainsField(ItemFields.MediaStreams)) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 52f14b0b1..774d3563c 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.HttpServer WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - using var connection = new WebSocketConnection( + var connection = new WebSocketConnection( _loggerFactory.CreateLogger<WebSocketConnection>(), webSocket, authorizationInfo, @@ -56,17 +56,19 @@ namespace Emby.Server.Implementations.HttpServer { OnReceive = ProcessWebSocketMessageReceived }; - - var tasks = new Task[_webSocketListeners.Length]; - for (var i = 0; i < _webSocketListeners.Length; ++i) + await using (connection.ConfigureAwait(false)) { - tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context); - } + var tasks = new Task[_webSocketListeners.Length]; + for (var i = 0; i < _webSocketListeners.Length; ++i) + { + tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context); + } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks).ConfigureAwait(false); - await connection.ReceiveAsync().ConfigureAwait(false); - _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + await connection.ReceiveAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + } } catch (Exception ex) // Otherwise ASP.Net will ignore the exception { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a2abafd2a..0c854bdb7 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2677,7 +2677,12 @@ namespace Emby.Server.Implementations.Library extra = itemById; } - extra.ExtraType = extraType; + // Only update extra type if it is more specific then the currently known extra type + if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown) + { + extra.ExtraType = extraType; + } + extra.ParentId = Guid.Empty; extra.OwnerId = owner.Id; return extra; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 18ada6aeb..9658bd566 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library if (user is not null) { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + SetDefaultAudioAndSubtitleStreamIndices(item, source, user); if (item.MediaType == MediaType.Audio) { @@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Error getting media sources"); - return Enumerable.Empty<MediaSourceInfo>(); + return []; } } @@ -339,7 +339,7 @@ namespace Emby.Server.Implementations.Library { foreach (var source in sources) { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + SetDefaultAudioAndSubtitleStreamIndices(item, source, user); if (item.MediaType == MediaType.Audio) { @@ -360,7 +360,7 @@ namespace Emby.Server.Implementations.Library { if (string.IsNullOrEmpty(language)) { - return Array.Empty<string>(); + return []; } var culture = _localizationManager.FindLanguageInfo(language); @@ -369,14 +369,15 @@ namespace Emby.Server.Implementations.Library return culture.ThreeLetterISOLanguageNames; } - return new string[] { language }; + return [language]; } private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { if (userData.SubtitleStreamIndex.HasValue && user.RememberSubtitleSelections - && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) + && user.SubtitleMode != SubtitlePlaybackMode.None + && allowRememberingSelection) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid @@ -390,7 +391,7 @@ namespace Emby.Server.Implementations.Library var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; - var audioLangage = defaultAudioIndex is null + var audioLanguage = defaultAudioIndex is null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); @@ -398,9 +399,9 @@ namespace Emby.Server.Implementations.Library source.MediaStreams, preferredSubs, user.SubtitleMode, - audioLangage); + audioLanguage); - MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); + MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage); } private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) @@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.Library source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } - public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) + public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request var mediaType = item?.MediaType ?? MediaType.Video; @@ -526,7 +527,7 @@ namespace Emby.Server.Implementations.Library var item = request.ItemId.IsEmpty() ? null : _libraryManager.GetItemById(request.ItemId); - SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); + SetDefaultAudioAndSubtitleStreamIndices(item, clone, user); } return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 6aef87c52..ea223e3ec 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -124,16 +124,16 @@ namespace Emby.Server.Implementations.Library } else if (mode == SubtitlePlaybackMode.Always) { - // always load the most suitable full subtitles + // Always load the most suitable full subtitles filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // always load the most suitable full subtitles + // Always load the most suitable full subtitles filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); } - // load forced subs if we have found no suitable full subtitles + // Load forced subs if we have found no suitable full subtitles var iterStreams = filteredStreams is null || filteredStreams.Count == 0 ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) : filteredStreams; diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 05af8d8a5..77643505e 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -125,5 +125,7 @@ "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры", "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.", "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay", - "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках." + "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.", + "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання", + "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць." } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index c4d8c6947..b7633f77c 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -126,5 +126,7 @@ "External": "Extern", "HearingImpaired": "Discapacitat auditiva", "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps", - "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades." + "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.", + "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", + "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 1c7bc75b5..2fa1c19e3 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -126,5 +126,7 @@ "External": "Externí", "HearingImpaired": "Sluchově postižení", "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay", - "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno." + "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.", + "TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání", + "TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání." } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 7a4c2067b..d8b2f828f 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -126,5 +126,7 @@ "External": "Extern", "HearingImpaired": "Hörgeschädigt", "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", - "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken." + "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken.", + "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen", + "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten." } diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 32bf89310..ff0c3d23d 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -126,5 +126,7 @@ "External": "External", "HearingImpaired": "Hearing Impaired", "TaskRefreshTrickplayImages": "Generate Trickplay Images", - "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries." + "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.", + "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", + "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist." } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 496ecabd3..4ba31bee0 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -125,5 +125,7 @@ "TaskOptimizeDatabase": "Optimize database", "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.", "TaskKeyframeExtractor": "Keyframe Extractor", - "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time." + "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", + "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", + "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist." } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index fe10be308..91e29d926 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} se ha añadido a la biblioteca", "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "LabelIpAddressValue": "Dirección IP: {0}", - "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", + "LabelRunningTimeValue": "Duración: {0}", "Latest": "Últimas", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 9bbf1dc29..db83d4b47 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -1,6 +1,6 @@ { "Albums": "Albums", - "AppDeviceValues": "Application: {0}, Appareil: {1}", + "AppDeviceValues": "Application : {0}, Appareil : {1}", "Application": "Application", "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", @@ -29,7 +29,7 @@ "Inherit": "Hériter", "ItemAddedWithName": "{0} a été ajouté à la médiathèque", "ItemRemovedWithName": "{0} a été supprimé de la médiathèque", - "LabelIpAddressValue": "Adresse IP: {0}", + "LabelIpAddressValue": "Adresse IP : {0}", "LabelRunningTimeValue": "Durée : {0}", "Latest": "Derniers", "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", @@ -126,5 +126,7 @@ "External": "Externe", "HearingImpaired": "Malentendants", "TaskRefreshTrickplayImages": "Générer des images Trickplay", - "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées." + "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.", + "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture", + "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus." } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index e7279994b..004ce68f5 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -126,5 +126,7 @@ "External": "Išorinis", "HearingImpaired": "Su klausos sutrikimais", "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus", - "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose." + "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.", + "TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose.", + "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index a925b7134..894d4b8ea 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -126,5 +126,7 @@ "External": "Extern", "HearingImpaired": "Slechthorend", "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren", - "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld." + "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.", + "TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen", + "TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten." } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index bd572b744..64427b459 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -126,5 +126,7 @@ "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", "HearingImpaired": "Niedosłyszący", "TaskRefreshTrickplayImages": "Generuj obrazy trickplay", - "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach." + "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.", + "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.", + "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 92ac2681e..dc96088ff 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -126,5 +126,7 @@ "External": "Externo", "HearingImpaired": "Surdo", "TaskRefreshTrickplayImages": "Gerar imagens de truques", - "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas." + "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.", + "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", + "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 103393a1e..de487488e 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -125,5 +125,7 @@ "TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.", "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo", - "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas." + "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.", + "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", + "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 26d678a0c..3d3f88709 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -126,5 +126,7 @@ "External": "Внешние", "HearingImpaired": "Для слабослышащих", "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay", - "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена." + "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.", + "TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения", + "TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют." } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 43594a42e..905dba5ab 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -126,5 +126,7 @@ "External": "Externé", "HearingImpaired": "Sluchovo postihnutí", "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay", - "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach." + "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.", + "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty", + "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú." } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index d7a627d12..059753957 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -126,5 +126,7 @@ "External": "Harici", "HearingImpaired": "Duyma Engelli", "TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur", - "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur." + "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.", + "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.", + "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 6f0dcfbe3..5f97d1ef9 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -83,7 +83,7 @@ "SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}", "StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.", "Songs": "Пісні", - "Shows": "Телепередачі", + "Shows": "Серіали", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ScheduledTaskStartedWithName": "{0} розпочато", "ScheduledTaskFailedWithName": "{0} незавершено, збій", @@ -125,5 +125,7 @@ "External": "Зовнішній", "HearingImpaired": "З порушеннями слуху", "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.", - "TaskRefreshTrickplayImages": "Створення Trickplay-зображень" + "TaskRefreshTrickplayImages": "Створити Trickplay-зображення", + "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення", + "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують." } diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index 65c8599e7..59185cdb7 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -9,37 +8,35 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// <summary> - /// Class AlbumArtistComparer. + /// Allows comparing artists of albums. Only the first artist of each album is considered. /// </summary> public class AlbumArtistComparer : IBaseItemComparer { /// <summary> - /// Gets the name. + /// Gets the item type this comparer compares. /// </summary> - /// <value>The name.</value> public ItemSortBy Type => ItemSortBy.AlbumArtist; /// <summary> - /// Compares the specified x. + /// Compares the specified arguments on their primary artist. /// </summary> - /// <param name="x">The x.</param> - /// <param name="y">The y.</param> - /// <returns>System.Int32.</returns> + /// <param name="x">First item to compare.</param> + /// <param name="y">Second item to compare.</param> + /// <returns>Zero if equal, else negative or positive number to indicate order.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); + return string.Compare(GetFirstAlbumArtist(x), GetFirstAlbumArtist(y), StringComparison.OrdinalIgnoreCase); } - /// <summary> - /// Gets the value. - /// </summary> - /// <param name="x">The x.</param> - /// <returns>System.String.</returns> - private static string? GetValue(BaseItem? x) + private static string? GetFirstAlbumArtist(BaseItem? x) { - var audio = x as IHasAlbumArtist; + if (x is IHasAlbumArtist audio + && audio.AlbumArtists.Count != 0) + { + return audio.AlbumArtists[0]; + } - return audio?.AlbumArtists.FirstOrDefault(); + return null; } } } diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index cd09d2bfa..72be55513 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task<ActionResult> GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -132,8 +132,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -261,12 +261,12 @@ public class AudioController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -296,8 +296,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 8db22f7eb..abe8bec2d 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -125,12 +125,15 @@ public class ConfigurationController : BaseJellyfinApiController /// <param name="mediaEncoderPath">Media encoder path form body.</param> /// <response code="204">Media encoder path updated.</response> /// <returns>Status.</returns> + [Obsolete("This endpoint is obsolete.")] + [ApiExplorerSettings(IgnoreApi = true)] [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath) { - _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + // API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES + // _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index e5be48b80..49fc2f3d7 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -163,18 +163,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task<ActionResult> GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -204,8 +204,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -406,12 +406,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -443,8 +443,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -577,12 +577,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -613,8 +613,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -742,12 +742,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -779,8 +779,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -909,12 +909,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -945,8 +945,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1085,12 +1085,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1122,8 +1122,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1265,12 +1265,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1301,8 +1301,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 984dc7789..360389d29 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -520,7 +520,11 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden) { - var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); + var items = _libraryManager.GetUserRootFolder().Children + .Concat(_libraryManager.RootFolder.VirtualChildren) + .Where(i => _libraryManager.GetLibraryOptions(i).Enabled) + .OrderBy(i => i.SortName) + .ToList(); if (isHidden.HasValue) { diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index cc2a630e1..e2c5486d9 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -165,7 +165,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="200">File returned.</response> /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns> [HttpGet("Providers/Subtitles/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] [ProducesFile("text/*")] diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 634fca2eb..db78e9946 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -92,13 +92,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index b3029d6fa..380120032 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -311,18 +311,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task<ActionResult> GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -354,8 +354,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -555,12 +555,12 @@ public class VideosController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -592,8 +592,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 5eec1b0ca..ec67b4c1b 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -192,7 +192,7 @@ public static class HlsCodecStringHelpers /// <returns>The AV1 codec string.</returns> public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) { - // https://aomedia.org/av1/specification/annex-a/ + // https://aomediacodec.github.io/av1-isobmff/#codecsparam // FORMAT: [codecTag].[profile].[level][tier].[bitDepth] StringBuilder result = new StringBuilder("av01", 13); @@ -214,8 +214,7 @@ public static class HlsCodecStringHelpers result.Append(".0"); } - if (level <= 0 - || level > 31) + if (level is <= 0 or > 31) { // Default to the maximum defined level 6.3 level = 19; @@ -230,7 +229,8 @@ public static class HlsCodecStringHelpers } result.Append('.') - .Append(level) + // Needed to pad it double digits; otherwise, browsers will reject the stream. + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", level) .Append(tierFlag ? 'H' : 'M'); string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index 12ce19368..b72dcff88 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -55,12 +55,12 @@ public class ClientCapabilitiesDto // TODO: Remove after 10.9 [Obsolete("Unused")] [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } + public bool? SupportsContentUploading { get; set; } = false; // TODO: Remove after 10.9 [Obsolete("Unused")] [DefaultValue(false)] - public bool? SupportsSync { get; set; } + public bool? SupportsSync { get; set; } = false; #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// <summary> diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 095bc9ed3..fed5dab69 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -141,6 +141,7 @@ public class TrickplayManager : ITrickplayManager width, TimeSpan.FromMilliseconds(options.Interval), options.EnableHwAcceleration, + options.EnableHwEncoding, options.ProcessThreads, options.Qscale, options.ProcessPriority, diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index bace703ad..44a1a85e3 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -138,7 +138,7 @@ namespace MediaBrowser.Controller.Library MediaProtocol GetPathProtocol(string path); - void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user); + void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user); Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 178a9999c..717b53a0b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1,6 +1,8 @@ #nullable disable #pragma warning disable CS1591 +// We need lowercase normalized string for ffmpeg +#pragma warning disable CA1308 using System; using System.Collections.Generic; @@ -26,6 +28,14 @@ namespace MediaBrowser.Controller.MediaEncoding { public partial class EncodingHelper { + /// <summary> + /// The codec validation regex. + /// This regular expression matches strings that consist of alphanumeric characters, hyphens, + /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. + /// This should matches all common valid codecs. + /// </summary> + public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + private const string QsvAlias = "qs"; private const string VaapiAlias = "va"; private const string D3d11vaAlias = "dx11"; @@ -53,6 +63,8 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private readonly Version _minFFmpegReadrateOption = new Version(5, 0); + private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); + private static readonly string[] _videoProfilesH264 = new[] { "ConstrainedBaseline", @@ -95,7 +107,6 @@ namespace MediaBrowser.Controller.MediaEncoding { "wmav2", 2 }, { "libmp3lame", 2 }, { "libfdk_aac", 6 }, - { "aac_at", 6 }, { "ac3", 6 }, { "eac3", 6 }, { "dca", 6 }, @@ -391,7 +402,10 @@ namespace MediaBrowser.Controller.MediaEncoding return "libtheora"; } - return codec.ToLowerInvariant(); + if (_validationRegex.IsMatch(codec)) + { + return codec.ToLowerInvariant(); + } } return "copy"; @@ -429,7 +443,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetInputFormat(string container) { - if (string.IsNullOrEmpty(container)) + if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container)) { return null; } @@ -685,6 +699,11 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = state.OutputAudioCodec; + if (!_validationRegex.IsMatch(codec)) + { + codec = "aac"; + } + if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { // Use Apple's aac encoder if available as it provides best audio quality @@ -732,6 +751,15 @@ namespace MediaBrowser.Controller.MediaEncoding return "dca"; } + if (string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase)) + { + // The ffmpeg upstream breaks the AudioToolbox ALAC encoder in version 6.1 but fixes it in version 7.0. + // Since ALAC is lossless in quality and the AudioToolbox encoder is not faster, + // its only benefit is a smaller file size. + // To prevent problems, use the ffmpeg native encoder instead. + return "alac"; + } + return codec.ToLowerInvariant(); } @@ -1313,7 +1341,7 @@ namespace MediaBrowser.Controller.MediaEncoding return ".ts"; } - public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) + private string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) { if (state.OutputVideoBitrate is null) { @@ -1382,7 +1410,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // The `maxrate` and `bufsize` options can potentially lead to performance regression // and even encoder hangs, especially when the value is very high. - return FormattableString.Invariant($" -b:v {bitrate}"); + return FormattableString.Invariant($" -b:v {bitrate} -qmin -1 -qmax -1"); } return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); @@ -5099,11 +5127,6 @@ namespace MediaBrowser.Controller.MediaEncoding /* Make main filters for video stream */ var mainFilters = new List<string>(); - // INPUT videotoolbox/memory surface(vram/uma) - // this will pass-through automatically if in/out format matches. - mainFilters.Add("format=nv12|p010le|videotoolbox_vld"); - mainFilters.Add("hwupload=derive_device=videotoolbox"); - // hw deint if (doDeintH2645) { @@ -5159,6 +5182,21 @@ namespace MediaBrowser.Controller.MediaEncoding overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0"); } + var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) || + subFilters.Any(f => !string.IsNullOrEmpty(f)) || + overlayFilters.Any(f => !string.IsNullOrEmpty(f)); + + // This is a workaround for ffmpeg's hwupload implementation + // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame + // will cause the encoder to produce incorrect frames. + if (needFiltering) + { + // INPUT videotoolbox/memory surface(vram/uma) + // this will pass-through automatically if in/out format matches. + mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld"); + mainFilters.Insert(0, "hwupload=derive_device=videotoolbox"); + } + return (mainFilters, subFilters, overlayFilters); } @@ -6630,7 +6668,7 @@ namespace MediaBrowser.Controller.MediaEncoding while (shiftAudioCodecs.Contains(audioCodecs[0], StringComparison.OrdinalIgnoreCase)) { - var removed = shiftAudioCodecs[0]; + var removed = audioCodecs[0]; audioCodecs.RemoveAt(0); audioCodecs.Add(removed); } @@ -6664,7 +6702,7 @@ namespace MediaBrowser.Controller.MediaEncoding while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparison.OrdinalIgnoreCase)) { - var removed = shiftVideoCodecs[0]; + var removed = videoCodecs[0]; videoCodecs.RemoveAt(0); videoCodecs.Add(removed); } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index c2cef4978..e696fa52c 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -149,6 +149,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="maxWidth">The maximum width.</param> /// <param name="interval">The interval.</param> /// <param name="allowHwAccel">Allow for hardware acceleration.</param> + /// <param name="enableHwEncoding">Use hardware mjpeg encoder.</param> /// <param name="threads">The input/output thread count for ffmpeg.</param> /// <param name="qualityScale">The qscale value for ffmpeg.</param> /// <param name="priority">The process priority for the ffmpeg process.</param> @@ -163,6 +164,7 @@ namespace MediaBrowser.Controller.MediaEncoding int maxWidth, TimeSpan interval, bool allowHwAccel, + bool enableHwEncoding, int? threads, int? qualityScale, ProcessPriorityClass? priority, diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 219da309e..06386f2b8 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.Net SingleWriter = false }); - private readonly SemaphoreSlim _lock = new(1, 1); + private readonly object _activeConnectionsLock = new(); /// <summary> /// The _active connections. @@ -126,15 +126,10 @@ namespace MediaBrowser.Controller.Net InitialDelayMs = dueTimeMs }; - _lock.Wait(); - try + lock (_activeConnectionsLock) { _activeConnections.Add((message.Connection, cancellationTokenSource, state)); } - finally - { - _lock.Release(); - } } protected void SendData(bool force) @@ -153,8 +148,7 @@ namespace MediaBrowser.Controller.Net (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples; var now = DateTime.UtcNow; - await _lock.WaitAsync().ConfigureAwait(false); - try + lock (_activeConnectionsLock) { if (_activeConnections.Count == 0) { @@ -174,10 +168,6 @@ namespace MediaBrowser.Controller.Net }) .ToArray(); } - finally - { - _lock.Release(); - } if (tuples.Length == 0) { @@ -240,8 +230,7 @@ namespace MediaBrowser.Controller.Net /// <param name="message">The message.</param> private void Stop(WebSocketMessageInfo message) { - _lock.Wait(); - try + lock (_activeConnectionsLock) { var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection); @@ -250,10 +239,6 @@ namespace MediaBrowser.Controller.Net DisposeConnection(connection); } } - finally - { - _lock.Release(); - } } /// <summary> @@ -283,15 +268,10 @@ namespace MediaBrowser.Controller.Net Logger.LogError(ex, "Error disposing websocket"); } - _lock.Wait(); - try + lock (_activeConnectionsLock) { _activeConnections.Remove(connection); } - finally - { - _lock.Release(); - } } protected virtual async ValueTask DisposeAsyncCore() @@ -306,18 +286,13 @@ namespace MediaBrowser.Controller.Net Logger.LogError(ex, "Disposing the message consumer failed"); } - await _lock.WaitAsync().ConfigureAwait(false); - try + lock (_activeConnectionsLock) { foreach (var connection in _activeConnections.ToArray()) { DisposeConnection(connection); } } - finally - { - _lock.Release(); - } } /// <inheritdoc /> diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 3a12a56f1..76d5d3a3f 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -134,6 +134,7 @@ namespace MediaBrowser.Controller.Session /// <value>The now playing item.</value> public BaseItemDto NowPlayingItem { get; set; } + [JsonIgnore] public BaseItem FullNowPlayingItem { get; set; } public BaseItemDto NowViewingItem { get; set; } diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 894aebed4..9aa9c3548 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.LocalMetadata.Images "folder", "poster", "cover", + "jacket", "default" }; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index ae0284e3a..5f0779dc7 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -69,6 +69,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "aac_at", "libfdk_aac", "ac3", + "alac", "dca", "libmp3lame", "libopus", diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 8278015d6..807678025 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -154,12 +154,12 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> public void SetFFmpegPath() { - // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence - var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath; + // 1) Check if the --ffmpeg CLI switch has been given + var ffmpegPath = _startupOptionFFmpegPath; if (string.IsNullOrEmpty(ffmpegPath)) { - // 2) Check if the --ffmpeg CLI switch has been given - ffmpegPath = _startupOptionFFmpegPath; + // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fallback + ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath; if (string.IsNullOrEmpty(ffmpegPath)) { // 3) Check "ffmpeg" @@ -463,6 +463,11 @@ namespace MediaBrowser.MediaEncoding.Encoder extraArgs += " -user_agent " + userAgent; } + if (request.MediaSource.Protocol == MediaProtocol.Rtsp) + { + extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp"; + } + return extraArgs; } @@ -800,6 +805,7 @@ namespace MediaBrowser.MediaEncoding.Encoder int maxWidth, TimeSpan interval, bool allowHwAccel, + bool enableHwEncoding, int? threads, int? qualityScale, ProcessPriorityClass? priority, @@ -828,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Encoder MediaPath = inputFile, OutputVideoCodec = "mjpeg" }; - var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec; + var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec; // Get input and filter arguments var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim(); diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index ab6f0d867..9a192f584 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -51,7 +51,6 @@ public class EncodingOptions EnableHardwareEncoding = true; AllowHevcEncoding = false; AllowAv1Encoding = false; - AllowMjpegEncoding = false; EnableSubtitleExtraction = true; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; @@ -263,11 +262,6 @@ public class EncodingOptions public bool AllowAv1Encoding { get; set; } /// <summary> - /// Gets or sets a value indicating whether MJPEG encoding is enabled. - /// </summary> - public bool AllowMjpegEncoding { get; set; } - - /// <summary> /// Gets or sets a value indicating whether subtitle extraction is enabled. /// </summary> public bool EnableSubtitleExtraction { get; set; } diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index 92c16ee84..a151d3429 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -14,6 +14,11 @@ public class TrickplayOptions public bool EnableHwAcceleration { get; set; } = false; /// <summary> + /// Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding. + /// </summary> + public bool EnableHwEncoding { get; set; } = false; + + /// <summary> /// Gets or sets the behavior used by trickplay provider on library scan/update. /// </summary> public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index cfff717db..6d5c84e1d 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Model.Dto public DateTime? DateLastMediaAdded { get; set; } - public string ExtraType { get; set; } + public ExtraType? ExtraType { get; set; } public int? AirsBeforeSeasonNumber { get; set; } diff --git a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs index e1894d84a..fc4cfdd66 100644 --- a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs +++ b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs @@ -1,16 +1,49 @@ -#pragma warning disable CS1591 +#pragma warning disable SA1300 // Lowercase required for backwards compat. -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// The collection type options. +/// </summary> +public enum CollectionTypeOptions { - public enum CollectionTypeOptions - { - Movies = 0, - TvShows = 1, - Music = 2, - MusicVideos = 3, - HomeVideos = 4, - BoxSets = 5, - Books = 6, - Mixed = 7 - } + /// <summary> + /// Movies. + /// </summary> + movies = 0, + + /// <summary> + /// TV Shows. + /// </summary> + tvshows = 1, + + /// <summary> + /// Music. + /// </summary> + music = 2, + + /// <summary> + /// Music Videos. + /// </summary> + musicvideos = 3, + + /// <summary> + /// Home Videos (and Photos). + /// </summary> + homevideos = 4, + + /// <summary> + /// Box Sets. + /// </summary> + boxsets = 5, + + /// <summary> + /// Books. + /// </summary> + books = 6, + + /// <summary> + /// Mixed Movies and TV Shows. + /// </summary> + mixed = 7 } diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index 2b2bda12c..89bb72c3c 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -37,7 +37,6 @@ namespace MediaBrowser.Model.Entities /// Gets or sets the type of the collection. /// </summary> /// <value>The type of the collection.</value> - [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))] public CollectionTypeOptions? CollectionType { get; set; } public LibraryOptions LibraryOptions { get; set; } diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index 5f51fb21c..fc1f24ae1 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -35,11 +35,11 @@ namespace MediaBrowser.Model.Session // TODO: Remove after 10.9 [Obsolete("Unused")] [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } + public bool? SupportsContentUploading { get; set; } = false; // TODO: Remove after 10.9 [Obsolete("Unused")] [DefaultValue(false)] - public bool? SupportsSync { get; set; } + public bool? SupportsSync { get; set; } = false; } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f34034964..a9ebf7ec7 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1106,7 +1106,8 @@ namespace MediaBrowser.Providers.Manager var musicArtists = albums .Select(i => i.MusicArtist) - .Where(i => i is not null); + .Where(i => i is not null) + .Distinct(); var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, cancellationToken)); diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a158e5c86..4f6ed4469 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -554,9 +554,13 @@ public class SkiaEncoder : IImageEncoder /// <inheritdoc /> public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops) { - var splashBuilder = new SplashscreenBuilder(this); - var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); - splashBuilder.GenerateSplash(posters, backdrops, outputPath); + // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail. + if (posters.Count > 0 && backdrops.Count > 0) + { + var splashBuilder = new SplashscreenBuilder(this); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + splashBuilder.GenerateSplash(posters, backdrops, outputPath); + } } /// <inheritdoc /> diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs deleted file mode 100644 index cd582ced6..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Jellyfin.Extensions.Json.Converters -{ - /// <summary> - /// Converts an object to a lowercase string. - /// </summary> - /// <typeparam name="T">The object type.</typeparam> - public class JsonLowerCaseConverter<T> : JsonConverter<T> - { - /// <inheritdoc /> - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonSerializer.Deserialize<T>(ref reader, options); - } - - /// <inheritdoc /> - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value?.ToString()?.ToLowerInvariant()); - } - } -} diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 92605a1eb..2f4caa386 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -159,7 +159,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable { Locations = [customPath], Name = "Recorded Movies", - CollectionType = CollectionTypeOptions.Movies + CollectionType = CollectionTypeOptions.movies }; } @@ -172,7 +172,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable { Locations = [customPath], Name = "Recorded Shows", - CollectionType = CollectionTypeOptions.TvShows + CollectionType = CollectionTypeOptions.tvshows }; } } diff --git a/src/Jellyfin.Networking/AutoDiscoveryHost.cs b/src/Jellyfin.Networking/AutoDiscoveryHost.cs index 5624c4ed1..2be57d7a1 100644 --- a/src/Jellyfin.Networking/AutoDiscoveryHost.cs +++ b/src/Jellyfin.Networking/AutoDiscoveryHost.cs @@ -78,28 +78,36 @@ public sealed class AutoDiscoveryHost : BackgroundService private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken) { - using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber)); - udpClient.MulticastLoopback = false; - - while (!cancellationToken.IsCancellationRequested) + try { - try + using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber)); + udpClient.MulticastLoopback = false; + + while (!cancellationToken.IsCancellationRequested) { - var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); - var text = Encoding.UTF8.GetString(result.Buffer); - if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + try { - await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var text = Encoding.UTF8.GetString(result.Buffer); + if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + { + await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) + { + _logger.LogError(ex, "Failed to receive data from socket"); } } - catch (SocketException ex) - { - _logger.LogError(ex, "Failed to receive data from socket"); - } - catch (OperationCanceledException) - { - _logger.LogDebug("Broadcast socket operation cancelled"); - } + } + catch (OperationCanceledException) + { + _logger.LogDebug("Broadcast socket operation cancelled"); + } + catch (Exception ex) + { + // Exception in this function will prevent the background service from restarting in-process. + _logger.LogError(ex, "Unable to bind to {Address}:{Port}", address, PortNumber); } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs deleted file mode 100644 index 16c69ca48..000000000 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Jellyfin.Extensions.Json.Converters; -using MediaBrowser.Model.Entities; -using Xunit; - -namespace Jellyfin.Extensions.Tests.Json.Converters -{ - public class JsonLowerCaseConverterTests - { - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() - { - Converters = - { - new JsonStringEnumConverter() - } - }; - - [Theory] - [InlineData(null, "{\"CollectionType\":null}")] - [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")] - [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")] - public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected) - { - Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions)); - } - - [Theory] - [InlineData("{\"CollectionType\":null}", null)] - [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)] - [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)] - public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result) - { - var res = JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions); - Assert.NotNull(res); - Assert.Equal(result, res!.CollectionType); - } - - [Theory] - [InlineData(null)] - [InlineData(CollectionTypeOptions.Movies)] - [InlineData(CollectionTypeOptions.MusicVideos)] - public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value) - { - var res = JsonSerializer.Deserialize<TestContainer>(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions); - Assert.NotNull(res); - Assert.Equal(value, res!.CollectionType); - } - - [Theory] - [InlineData("{\"CollectionType\":null}")] - [InlineData("{\"CollectionType\":\"movies\"}")] - [InlineData("{\"CollectionType\":\"musicvideos\"}")] - public void RoundTrip_String_Correct(string json) - { - var res = JsonSerializer.Serialize(JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions), _jsonOptions); - Assert.Equal(json, res); - } - - private sealed class TestContainer - { - public TestContainer(CollectionTypeOptions? collectionType) - { - CollectionType = collectionType; - } - - [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))] - public CollectionTypeOptions? CollectionType { get; set; } - } - } -} |
