diff options
29 files changed, 295 insertions, 167 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 909f22ed1d..45235be712 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,6 +87,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: + - 10.11.8 - 10.11.7 - 10.11.6 - Master diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index dc8ba3ab3e..563a0a406f 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -63,14 +63,10 @@ jobs: name: openapi-base path: openapi-base - name: Detect Changes - runs-on: ubuntu-latest id: openapi-diff - with: - old-spec: openapi-base/openapi.json - new-spec: openapi-head/openapi.json run: | - sed 's:allOf:oneOf:g' openapi-head/openapi.json - sed 's:allOf:oneOf:g' openapi-base/openapi.json + sed -i 's:allOf:oneOf:g' openapi-head/openapi.json + sed -i 's:allOf:oneOf:g' openapi-base/openapi.json mkdir -p /tmp/openapi-report mv openapi-head/openapi.json /tmp/openapi-report/head.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 3385ee070a..f15f7c7a75 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageVersion Include="System.Text.Json" Version="10.0.5" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.11.0" /> + <PackageVersion Include="z440.atl.core" Version="7.12.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index b392340f71..08ced387b8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1019,6 +1019,15 @@ namespace Emby.Server.Implementations.Dto { dto.AlbumId = albumParent.Id; dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary); + if (albumParent.LUFS.HasValue) + { + // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0 + dto.AlbumNormalizationGain = -18f - albumParent.LUFS; + } + else if (albumParent.NormalizationGain.HasValue) + { + dto.AlbumNormalizationGain = albumParent.NormalizationGain; + } } // if (options.ContainsField(ItemFields.MediaSourceCount)) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 1e7279be83..fce3a614c2 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -63,8 +63,8 @@ "Photos": "Fotos", "Playlists": "Llistes de reproducció", "Plugin": "Complement", - "PluginInstalledWithName": "{0} ha estat instal·lat", - "PluginUninstalledWithName": "S'ha instal·lat {0}", + "PluginInstalledWithName": "{0} s'ha instal·lat", + "PluginUninstalledWithName": "{0} s'ha desinstal·lat", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index ae5bd6ce9e..76950467bd 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,5 +1,4 @@ { - "Albums": "Albums", "AppDeviceValues": "App: {0}, Apparaat: {1}", "Application": "Applicatie", "Artists": "Artiesten", @@ -14,7 +13,6 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteAlbums": "Favoriete albums", @@ -137,5 +135,7 @@ "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", "TaskExtractMediaSegments": "Scannen op mediasegmenten", "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.", - "CleanupUserDataTask": "Opruimtaak gebruikersdata" + "CleanupUserDataTask": "Opruimtaak gebruikersdata", + "Albums": "Albums", + "Genres": "Genres" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3ad772aa9c..26f49573e7 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -124,17 +124,17 @@ "TaskKeyframeExtractor": "Екстрактор ключових кадрів", "External": "Зовнішній", "HearingImpaired": "З порушеннями слуху", - "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.", - "TaskRefreshTrickplayImages": "Створити Trickplay-зображення", + "TaskRefreshTrickplayImagesDescription": "Створює прев'ю-зображення для відео у ввімкнених медіатеках.", + "TaskRefreshTrickplayImages": "Створити Прев'ю-зображення", "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення", "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.", "TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.", "TaskAudioNormalization": "Нормалізація аудіо", "TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень", "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень", - "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", + "TaskMoveTrickplayImagesDescription": "Переміщує наявні прев'ю-зображення відповідно до налаштувань медіатеки.", "TaskExtractMediaSegments": "Сканування медіа-сегментів", - "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень", + "TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень", "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.", "CleanupUserDataTask": "Завдання очищення даних користувача", "CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому." diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 17cd2a9a41..8ae899a73c 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -5,7 +5,7 @@ "Artists": "藝人", "AuthenticationSucceededWithUserName": "{0} 成功通過驗證", "Books": "書籍", - "CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相", + "CameraImageUploadedFrom": "{0} 已經成功上載咗一張新相", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "心水嘅節目", "HeaderFavoriteSongs": "心水嘅歌曲", "HeaderLiveTV": "電視直播", - "HeaderNextUp": "繼續觀看", + "HeaderNextUp": "跟住落嚟", "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", @@ -42,13 +42,13 @@ "MusicVideos": "MV", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", - "NameSeasonUnknown": "未知的季度", + "NameSeasonUnknown": "未知嘅季度", "NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。", "NotificationOptionApplicationUpdateAvailable": "有得更新應用程式", "NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗", "NotificationOptionAudioPlayback": "開始播放音訊", "NotificationOptionAudioPlaybackStopped": "停咗播放音訊", - "NotificationOptionCameraImageUploaded": "相機相片上傳咗", + "NotificationOptionCameraImageUploaded": "相機相片上載咗", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "加咗新內容", "NotificationOptionPluginError": "外掛程式錯誤", @@ -72,14 +72,14 @@ "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動", "Shows": "節目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。", + "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,唔該稍後再試。", "SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗", "Sync": "同步", "System": "系統", "TvShows": "電視節目", "User": "使用者", "UserCreatedWithName": "經已建立咗新使用者 {0}", - "UserDeletedWithName": "使用者 {0} 經已被刪除", + "UserDeletedWithName": "使用者 {0} 經已被刪走", "UserDownloadingItemWithValues": "{0} 下載緊 {1}", "UserLockedOutWithName": "使用者 {0} 經已被鎖定", "UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線", @@ -89,7 +89,7 @@ "UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}", "UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃", - "ValueSpecialEpisodeName": "特別篇 - {0}", + "ValueSpecialEpisodeName": "特輯 - {0}", "VersionNumber": "版本 {0}", "TaskDownloadMissingSubtitles": "下載漏咗嘅字幕", "TaskUpdatePlugins": "更新外掛程式", @@ -99,16 +99,16 @@ "TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。", "TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。", "TaskRefreshChannels": "重新載入頻道", - "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。", + "TaskCleanTranscodeDescription": "自動刪走超過一日嘅轉碼檔案。", "TaskCleanTranscode": "清理轉碼資料夾", "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。", "TaskRefreshPeopleDescription": "更新媒體櫃入面演員同導演嘅媒體詳細資料。", - "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。", + "TaskCleanLogsDescription": "自動刪走超過 {0} 日嘅紀錄檔。", "TaskCleanLogs": "清理日誌資料夾", "TaskRefreshLibrary": "掃描媒體櫃", "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。", "TaskRefreshChapterImages": "擷取章節圖片", - "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅快取檔案。", + "TaskCleanCacheDescription": "刪走系統已經唔再需要嘅快取檔案。", "TaskCleanCache": "清理快取(Cache)資料夾", "TasksChannelsCategory": "網路頻道", "TasksLibraryCategory": "媒體櫃", @@ -119,7 +119,7 @@ "Default": "初始", "TaskOptimizeDatabaseDescription": "壓縮數據櫃並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據櫃嘅操作之後行呢個任務,或者可以提升效能。", "TaskOptimizeDatabase": "最佳化數據櫃", - "TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。", + "TaskCleanActivityLogDescription": "刪走超過設定日期嘅活動記錄。", "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。", "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 8e14f5bdf4..6b8888d244 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -960,7 +960,7 @@ namespace Emby.Server.Implementations.Session } var tracksChanged = UpdatePlaybackSettings(user, info, data); - if (!tracksChanged) + if (tracksChanged) { changed = true; } diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 4be79ff5a0..590bd05da4 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] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task<ActionResult> GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index acd5dd64ec..c13da3ac7b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task<ActionResult> GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -187,7 +187,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -206,8 +206,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -412,12 +412,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -427,7 +427,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -448,8 +448,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -585,12 +585,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -601,7 +601,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -620,8 +620,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -752,12 +752,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -767,7 +767,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -788,8 +788,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -921,12 +921,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -937,7 +937,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -956,8 +956,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1091,7 +1091,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1099,12 +1099,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1114,7 +1114,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1135,8 +1135,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1273,7 +1273,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1281,12 +1281,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1297,7 +1297,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1316,8 +1316,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 091a0c8c73..39760556a6 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -11,6 +11,7 @@ using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -270,15 +271,17 @@ public class ItemsController : BaseJellyfinApiController var dtoOptions = new DtoOptions { Fields = fields } .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var item = _libraryManager.GetParentItem(parentId, userId); + QueryResult<BaseItem> result; + if (includeItemTypes.Length == 1 - && includeItemTypes[0] == BaseItemKind.BoxSet) + && includeItemTypes[0] == BaseItemKind.BoxSet + && item is not BoxSet) { parentId = null; + item = _libraryManager.GetUserRootFolder(); } - var item = _libraryManager.GetParentItem(parentId, userId); - QueryResult<BaseItem> result; - if (item is not Folder folder) { folder = _libraryManager.GetUserRootFolder(); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 94f62a0713..9a32a303a9 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -454,7 +454,7 @@ public class LiveTvController : BaseJellyfinApiController /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId) { await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); @@ -976,7 +976,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Created tuner host returned.</response> /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns> [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); @@ -988,7 +988,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="204">Tuner host deleted.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpDelete("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { @@ -1021,7 +1021,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Created listings provider returned.</response> /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns> [HttpPost("ListingProviders")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider( @@ -1047,7 +1047,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="204">Listing provider deleted.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpDelete("ListingProviders")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { @@ -1080,7 +1080,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Available countries returned.</response> /// <returns>A <see cref="FileResult"/> containing the available countries.</returns> [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task<ActionResult> GetSchedulesDirectCountries() @@ -1101,7 +1101,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Channel mapping options returned.</response> /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns> [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId) => _listingsManager.GetChannelMappingOptions(providerId); @@ -1113,7 +1113,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Created channel mapping returned.</response> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto) => _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId); @@ -1137,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the tuners.</returns> [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false) => _tunerHostManager.DiscoverTuners(newDevicesOnly); @@ -1185,7 +1185,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesVideoFile] public ActionResult GetLiveStreamFile( [FromRoute, Required] string streamId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 438d054a4c..2b2afb0fe6 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -47,6 +47,7 @@ public class PersonsController : BaseJellyfinApiController /// <summary> /// Gets all persons. /// </summary> + /// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="searchTerm">The search term.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> @@ -57,6 +58,7 @@ public class PersonsController : BaseJellyfinApiController /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param> /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param> + /// <param name="parentId">Optional. Specify this to localize the search to a specific library. Omit to use the root.</param> /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param> /// <param name="userId">User id.</param> /// <param name="enableImages">Optional, include image information in output.</param> @@ -65,6 +67,7 @@ public class PersonsController : BaseJellyfinApiController [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetPersons( + [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, @@ -75,6 +78,7 @@ public class PersonsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery] Guid? parentId, [FromQuery] Guid? appearsInItemId, [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) @@ -96,6 +100,8 @@ public class PersonsController : BaseJellyfinApiController User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, + ParentId = parentId, + StartIndex = startIndex, Limit = limit ?? 0 }); diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 3d6874079d..991fb87144 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController [FromBody, Required] NewGroupRequestDto requestData) { var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new NewGroupRequest(requestData.GroupName); + var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim()); return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None)); } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index b1a91ae70f..f4e0c86143 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -101,13 +101,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] 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 ccf8e90632..7854edc5ac 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -313,18 +313,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task<ActionResult> GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -334,7 +334,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -355,8 +355,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -551,18 +551,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task<ActionResult> GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -572,7 +572,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -593,8 +593,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index c6823fa807..bae2756303 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -17,9 +17,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers; @@ -422,14 +420,18 @@ public static class StreamingHelpers request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); break; case 4: - if (videoRequest is not null) + if (videoRequest is not null && IsValidCodecName(val)) { videoRequest.VideoCodec = val; } break; case 5: - request.AudioCodec = val; + if (IsValidCodecName(val)) + { + request.AudioCodec = val; + } + break; case 6: if (videoRequest is not null) @@ -483,7 +485,7 @@ public static class StreamingHelpers request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); break; case 15: - if (videoRequest is not null) + if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val)) { videoRequest.Level = val; } @@ -504,7 +506,7 @@ public static class StreamingHelpers break; case 18: - if (videoRequest is not null) + if (videoRequest is not null && IsValidCodecName(val)) { videoRequest.Profile = val; } @@ -563,7 +565,11 @@ public static class StreamingHelpers break; case 30: - request.SubtitleCodec = val; + if (IsValidCodecName(val)) + { + request.SubtitleCodec = val; + } + break; case 31: if (videoRequest is not null) @@ -586,6 +592,11 @@ public static class StreamingHelpers } } + private static bool IsValidCodecName(string val) + { + return EncodingHelper.ContainerValidationRegex().IsMatch(val); + } + /// <summary> /// Parses the container into its file extension. /// </summary> diff --git a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs index 32a3bb444c..2e1889fed4 100644 --- a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs +++ b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace Jellyfin.Api.Models.SyncPlayDtos; /// <summary> @@ -17,5 +19,6 @@ public class NewGroupRequestDto /// Gets or sets the group name. /// </summary> /// <value>The name of the new group.</value> + [StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")] public string GroupName { get; set; } } diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs index 149fc9042d..0fc8d3cd25 100644 --- a/Jellyfin.Data/UserEntityExtensions.cs +++ b/Jellyfin.Data/UserEntityExtensions.cs @@ -185,7 +185,7 @@ public static class UserEntityExtensions entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); - entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false)); entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index e2569241d2..ad9953d1b6 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -62,7 +62,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct(); - // dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.StartIndex.HasValue && filter.StartIndex > 0) + { + dbQuery = dbQuery.Skip(filter.StartIndex.Value); + } + if (filter.Limit > 0) { dbQuery = dbQuery.Take(filter.Limit); @@ -197,6 +201,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId))); } + if (filter.ParentId != null) + { + query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentId && i.ItemId == w.ItemId))); + } + if (!filter.AppearsInItemId.IsEmpty()) { query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId))); diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs index e82123e5ac..2b1f549940 100644 --- a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs +++ b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs @@ -7,7 +7,6 @@ using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -50,7 +49,7 @@ internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine foreach (var virtualFolder in virtualFolders) { var options = virtualFolder.LibraryOptions; - if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) + if (options?.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) { continue; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 2404ace751..e312e9d80b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1171,11 +1171,18 @@ namespace MediaBrowser.Controller.Entities info.Video3DFormat = video.Video3DFormat; info.Timestamp = video.Timestamp; - if (video.IsShortcut) + if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath)) { - info.IsRemote = true; - info.Path = video.ShortcutPath; - info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); + var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath); + + // Only allow remote shortcut paths — local file paths in .strm files + // could be used to read arbitrary files from the server. + if (shortcutProtocol != MediaProtocol.File) + { + info.IsRemote = true; + info.Path = video.ShortcutPath; + info.Protocol = shortcutProtocol; + } } if (string.IsNullOrEmpty(info.Container)) @@ -1600,7 +1607,6 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { - Logger.LogDebug("{0} has no parental rating set.", Name); return !GetBlockUnratedValue(user); } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 203a16a668..f4b3910b0e 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -21,6 +21,8 @@ namespace MediaBrowser.Controller.Entities ExcludePersonTypes = excludePersonTypes; } + public int? StartIndex { get; set; } + /// <summary> /// Gets or sets the maximum number of items the query should return. /// </summary> @@ -28,6 +30,8 @@ namespace MediaBrowser.Controller.Entities public Guid ItemId { get; set; } + public Guid? ParentId { get; set; } + public IReadOnlyList<string> PersonTypes { get; } public IReadOnlyList<string> ExcludePersonTypes { get; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f2468782ff..9f7e35d1ea 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding public partial class EncodingHelper { /// <summary> - /// The codec validation regex. + /// The codec validation regex string. /// 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 ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; /// <summary> - /// The level validation regex. + /// The level validation regex string. /// This regular expression matches strings representing a double. /// </summary> - public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?"; + public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?"; private const string _defaultMjpegEncoder = "mjpeg"; @@ -87,8 +87,6 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0); - private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); - private static readonly string[] _videoProfilesH264 = [ "ConstrainedBaseline", @@ -181,6 +179,22 @@ namespace MediaBrowser.Controller.MediaEncoding RemoveHdr10Plus, } + /// <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> + [GeneratedRegex(ContainerValidationRegexStr)] + public static partial Regex ContainerValidationRegex(); + + /// <summary> + /// The level validation regex string. + /// This regular expression matches strings representing a double. + /// </summary> + [GeneratedRegex(LevelValidationRegexStr)] + public static partial Regex LevelValidationRegex(); + [GeneratedRegex(@"\s+")] private static partial Regex WhiteSpaceRegex(); @@ -477,7 +491,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetMjpegEncoder(state, encodingOptions); } - if (_containerValidationRegex.IsMatch(codec)) + if (ContainerValidationRegex().IsMatch(codec)) { return codec.ToLowerInvariant(); } @@ -518,7 +532,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetInputFormat(string container) { - if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container)) + if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container)) { return null; } @@ -736,7 +750,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = state.OutputAudioCodec; - if (!_containerValidationRegex.IsMatch(codec)) + if (!ContainerValidationRegex().IsMatch(codec)) { codec = "aac"; } @@ -1790,38 +1804,40 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) + if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) + { + return null; + } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + // Transcode to level 5.3 (15) and lower for maximum compatibility. + // https://en.wikipedia.org/wiki/AV1#Levels + if (requestLevel < 0 || requestLevel >= 15) { - // Transcode to level 5.3 (15) and lower for maximum compatibility. - // https://en.wikipedia.org/wiki/AV1#Levels - if (requestLevel < 0 || requestLevel >= 15) - { - return "15"; - } + return "15"; } - else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + } + else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.0 and lower for maximum compatibility. + // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. + // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels + // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. + if (requestLevel < 0 || requestLevel >= 150) { - // Transcode to level 5.0 and lower for maximum compatibility. - // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. - // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels - // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. - if (requestLevel < 0 || requestLevel >= 150) - { - return "150"; - } + return "150"; } - else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + } + else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.1 and lower for maximum compatibility. + // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. + // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels + if (requestLevel < 0 || requestLevel >= 51) { - // Transcode to level 5.1 and lower for maximum compatibility. - // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. - // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels - if (requestLevel < 0 || requestLevel >= 51) - { - return "51"; - } + return "51"; } } @@ -2211,12 +2227,10 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var level = state.GetRequestedLevel(targetVideoCodec); + var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec)); if (!string.IsNullOrEmpty(level)) { - level = NormalizeTranscodingLevel(state, level); - // libx264, QSV, AMF can adjust the given level to match the output. if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8f223c12a5..e96bba0464 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -790,6 +790,12 @@ namespace MediaBrowser.Model.Dto public float? NormalizationGain { get; set; } /// <summary> + /// Gets or sets the gain required for audio normalization. This field is inherited from music album normalization gain. + /// </summary> + /// <value>The gain required for audio normalization.</value> + public float? AlbumNormalizationGain { get; set; } + + /// <summary> /// Gets or sets the current program. /// </summary> /// <value>The current program.</value> diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 9f5463b82c..c3ff26202f 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -262,9 +262,28 @@ namespace MediaBrowser.Providers.MediaInfo private void FetchShortcutInfo(BaseItem item) { - item.ShortcutPath = File.ReadAllLines(item.Path) + var shortcutPath = File.ReadAllLines(item.Path) .Select(NormalizeStrmLine) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); + + if (string.IsNullOrWhiteSpace(shortcutPath)) + { + return; + } + + // Only allow remote URLs in .strm files to prevent local file access + if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase))) + { + item.ShortcutPath = shortcutPath; + } + else + { + _logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath); + } } /// <summary> diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index ae5e1090ad..a78ec995cf 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Emby.Naming.Common; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; @@ -32,6 +33,7 @@ namespace MediaBrowser.Providers.Subtitles private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; private readonly ILocalizationManager _localization; + private readonly HashSet<string> _allowedSubtitleFormats; private readonly ISubtitleProvider[] _subtitleProviders; @@ -41,7 +43,8 @@ namespace MediaBrowser.Providers.Subtitles ILibraryMonitor monitor, IMediaSourceManager mediaSourceManager, ILocalizationManager localizationManager, - IEnumerable<ISubtitleProvider> subtitleProviders) + IEnumerable<ISubtitleProvider> subtitleProviders, + NamingOptions namingOptions) { _logger = logger; _fileSystem = fileSystem; @@ -51,6 +54,9 @@ namespace MediaBrowser.Providers.Subtitles _subtitleProviders = subtitleProviders .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) .ToArray(); + _allowedSubtitleFormats = new HashSet<string>( + namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')), + StringComparer.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -171,6 +177,12 @@ namespace MediaBrowser.Providers.Subtitles /// <inheritdoc /> public Task UploadSubtitle(Video video, SubtitleResponse response) { + var format = response.Format; + if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format)) + { + throw new ArgumentException($"Unsupported subtitle format: '{format}'"); + } + var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); return TrySaveSubtitle(video, libraryOptions, response); } @@ -193,7 +205,13 @@ namespace MediaBrowser.Providers.Subtitles } var savePaths = new List<string>(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + var language = response.Language.ToLowerInvariant(); + if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0) + { + throw new ArgumentException("Language contains invalid characters."); + } + + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language; if (response.IsForced) { @@ -221,15 +239,22 @@ namespace MediaBrowser.Providers.Subtitles private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension) { + if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Invalid subtitle format: {extension}"); + } + List<Exception>? exs = null; foreach (var savePath in savePaths) { - var path = savePath + "." + extension; + var path = Path.GetFullPath(savePath + "." + extension); try { - if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal) - || path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar; + var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar; + if (path.StartsWith(containingFolder, StringComparison.Ordinal) + || path.StartsWith(metadataFolder, StringComparison.Ordinal)) { var fileExists = File.Exists(path); var counter = 0; diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index 2270758454..5da7762f6f 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts } else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { + if (!IsValidChannelUrl(trimmedLine)) + { + _logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine); + extInf = string.Empty; + continue; + } + var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine); channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); @@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts return numberString; } + private static bool IsValidChannelUrl(string url) + { + return Uri.TryCreate(url, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase)); + } + private static bool IsValidChannelNumber(string numberString) { if (string.IsNullOrWhiteSpace(numberString) |
