aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml1
-rw-r--r--.github/workflows/openapi-pull-request.yml8
-rw-r--r--Directory.Packages.props2
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs9
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json22
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs24
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs76
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs11
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs20
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs24
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs25
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs3
-rw-r--r--Jellyfin.Data/UserEntityExtensions.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs11
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs3
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs16
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs90
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs21
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs35
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs17
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)