diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 47477ba602..6063851163 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf
Linux.amd64:
BuildConfiguration: linux.amd64
+ Linux.amd64-musl:
+ BuildConfiguration: linux.amd64-musl
+ Linux.arm64:
+ BuildConfiguration: linux.arm64
+ Linux.armhf:
+ BuildConfiguration: linux.armhf
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index bd30cc1e11..8b057a0950 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -78,9 +78,7 @@
-
-
-
+
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index fb4454a343..175a1ad2b1 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -126,6 +126,11 @@ namespace Emby.Dlna.Main
public static DlnaEntryPoint Current { get; private set; }
+ ///
+ /// Gets a value indicating whether the dlna server is enabled.
+ ///
+ public static bool Enabled { get; private set; }
+
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
@@ -152,6 +157,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
+ Enabled = options.EnableServer;
StartSsdpHandler();
diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs
index d56b1df507..e4a7a3a596 100644
--- a/Emby.Dlna/Profiles/SonyPs3Profile.cs
+++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
- AudioCodec = "ac3,mp2,mp3,aac"
+ AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
- AudioCodec = "ac3,aac,mp3",
+ AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile
diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs
index db56094e2b..985df0c9a5 100644
--- a/Emby.Dlna/Profiles/SonyPs4Profile.cs
+++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
- AudioCodec = "ac3,mp2,mp3,aac"
+ AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
- AudioCodec = "mp3",
+ AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
index bafa44b828..129b188e2a 100644
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
+++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
@@ -38,7 +38,7 @@
-
+
@@ -46,7 +46,7 @@
-
+
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
index eb8e645b31..592119305e 100644
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
+++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
@@ -38,7 +38,7 @@
-
+
@@ -46,7 +46,7 @@
-
+
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 0c94f937ce..b81476ea79 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -23,14 +23,6 @@
-
-
-
-
-
-
-
-
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 86914dea20..040b6b9e4e 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
- return GetUser((HttpContext)requestContext);
+ return GetUser(((HttpRequest)requestContext).HttpContext);
}
}
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e4221dd508..b6b7ea9495 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -129,23 +129,23 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
- list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
+ list = list.Where(i => !user.GetPreferenceValues(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
- var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
+ var orders = user.GetPreferenceValues(PreferenceKind.OrderedViews);
return list
.OrderBy(i =>
{
- var index = orders.IndexOf(i.Id.ToString("D", CultureInfo.InvariantCulture));
+ var index = Array.IndexOf(orders, i.Id);
if (index == -1
&& i is UserView view
&& view.DisplayParentId != Guid.Empty)
{
- index = orders.IndexOf(view.DisplayParentId.ToString("D", CultureInfo.InvariantCulture));
+ index = Array.IndexOf(orders, view.DisplayParentId);
}
return index == -1 ? int.MaxValue : index;
@@ -280,8 +280,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
- .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreferenceValues(PreferenceKind.LatestItemExcludes)
+ .Contains(i.Id))
.ToList();
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 90e6cc9668..b19ccadd8f 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -784,18 +784,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var allStations = root.stations ?? new List();
var map = root.map;
- int len = map.Count;
- var array = new List(len);
- for (int i = 0; i < len; i++)
+ var list = new List(map.Count);
+ foreach (var channel in map)
{
- var channelNumber = GetChannelNumber(map[i]);
+ var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase));
+ var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
if (station == null)
{
station = new ScheduleDirect.Station
{
- stationID = map[i].stationID
+ stationID = channel.stationID
};
}
@@ -812,10 +811,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
channelInfo.ImageUrl = station.logo.URL;
}
- array[i] = channelInfo;
+ list.Add(channelInfo);
}
- return array;
+ return list;
}
private static string NormalizeName(string value)
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index cd64cdde45..7667612b90 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "Library",
"TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
- "TaskCleanActivityLog": "Clean Activity Log"
+ "TaskCleanActivityLog": "Clean Activity Log",
+ "Undefined": "Undefined",
+ "Forced": "Forced",
+ "Default": "Default"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index dcd30694f0..03c6d5f5d3 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -112,5 +112,9 @@
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación",
- "AppDeviceValues": "App: {0}, Dispositivo: {1}"
+ "AppDeviceValues": "App: {0}, Dispositivo: {1}",
+ "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
+ "TaskCleanActivityLog": "Limpiar Registro de Actividades",
+ "Undefined": "Sin definir",
+ "Forced": "Forzado"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 02bf8496ff..fa0ab8b927 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -4,7 +4,7 @@
"Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
- "Books": "ブック",
+ "Books": "ブックス",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
@@ -114,5 +114,8 @@
"TaskRefreshChapterImages": "チャプター画像を抽出する",
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
- "TaskCleanActivityLog": "アクティビティの履歴を消去"
+ "TaskCleanActivityLog": "アクティビティの履歴を消去",
+ "Undefined": "未定義",
+ "Forced": "強制",
+ "Default": "デフォルト"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 91c1fb15bc..47c0879be4 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -91,5 +91,22 @@
"UserStoppedPlayingItemWithValues": "{0} - {1} oınatýyn {2} toqtatty",
"ValueHasBeenAddedToLibrary": "{0} (tasyǵyshhanaǵa ústelindi)",
"ValueSpecialEpisodeName": "Arnaıy - {0}",
- "VersionNumber": "Nusqasy {0}"
+ "VersionNumber": "Nusqasy {0}",
+ "Default": "Ádepki",
+ "TaskDownloadMissingSubtitles": "Joq sýbtıtrlerdi júktep alý",
+ "TaskRefreshChannels": "Arnalardy jańartý",
+ "TaskCleanTranscode": "Qaıta kodtaý katalogyn tazalaý",
+ "TaskUpdatePlugins": "Plagınderdi jańartý",
+ "TaskRefreshPeople": "Adamdardy jańartý",
+ "TaskCleanLogs": "Jurnal katalogyn tazalaý",
+ "TaskRefreshLibrary": "Tasyǵyshhanany skanerleý",
+ "TaskRefreshChapterImages": "Sahna keskinderin shyǵaryp alý",
+ "TaskCleanCache": "Kesh katalogyn tazalaý",
+ "TaskCleanActivityLog": "Áreket jurnalyn tazalaý",
+ "TasksChannelsCategory": "Internet-arnalar",
+ "TasksApplicationCategory": "Qoldanba",
+ "TasksLibraryCategory": "Tasyǵyshhana",
+ "TasksMaintenanceCategory": "Qyzmet kórsetý",
+ "Undefined": "Anyqtalmady",
+ "Forced": "Májbúrli"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index b6672a554a..1e80d0b5f9 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,7 +1,7 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Programma",
+ "Application": "Applicatie",
"Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
"Books": "Boeken",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index ca6172fce0..03d30247a7 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -96,7 +96,7 @@
"TaskRefreshChannels": "Обновление каналов",
"TaskCleanTranscode": "Очистка каталога перекодировки",
"TaskUpdatePlugins": "Обновление плагинов",
- "TaskRefreshPeople": "Обновление метаданных людей",
+ "TaskRefreshPeople": "Подновить людей",
"TaskCleanLogs": "Очистка каталога журналов",
"TaskRefreshLibrary": "Сканирование медиатеки",
"TaskRefreshChapterImages": "Извлечение изображений сцен",
@@ -109,7 +109,7 @@
"TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.",
"TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.",
"TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.",
- "TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.",
+ "TaskRefreshPeopleDescription": "Обновляются метаданные для актёров и режиссёров в медиатеке.",
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 8da92f3099..d785bcb907 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -66,7 +66,7 @@
"Inherit": "Наследи",
"HomeVideos": "Кућни видео",
"HeaderRecordingGroups": "Групе снимања",
- "HeaderNextUp": "Следеће горе",
+ "HeaderNextUp": "Следи",
"HeaderLiveTV": "ТВ уживо",
"HeaderFavoriteSongs": "Омиљене песме",
"HeaderFavoriteShows": "Омиљене серије",
@@ -79,17 +79,17 @@
"Folders": "Фасцикле",
"Favorites": "Омиљено",
"FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
- "DeviceOnlineWithName": "{0} се повезао",
+ "DeviceOnlineWithName": "{0} је повезан",
"DeviceOfflineWithName": "{0} је прекинуо везу",
"Collections": "Колекције",
"ChapterNameValue": "Поглавље {0}",
"Channels": "Канали",
- "CameraImageUploadedFrom": "Нова фотографија је послата са {0}",
+ "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге",
"AuthenticationSucceededWithUserName": "{0} успешно проверено",
- "Artists": "Извођач",
+ "Artists": "Извођачи",
"Application": "Апликација",
- "AppDeviceValues": "Апл: {0}, уређај: {1}",
+ "AppDeviceValues": "Апликација: {0}, Уређај: {1}",
"Albums": "Албуми",
"TaskDownloadMissingSubtitlesDescription": "Претражује интернет за недостајуће титлове на основу конфигурације метаподатака.",
"TaskDownloadMissingSubtitles": "Преузмите недостајуће титлове",
@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Брише логове старије од {0} дана.",
"TaskCleanLogs": "Очистите директоријум логова",
"TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.",
- "TaskRefreshLibrary": "Скенирај Библиотеку Медија",
+ "TaskRefreshLibrary": "Скенирај библиотеку медија",
"TaskRefreshChapterImagesDescription": "Ствара сличице за видео записе који имају поглавља.",
"TaskRefreshChapterImages": "Издвоји слике из поглавља",
"TaskCleanCacheDescription": "Брише Кеш фајлове који више нису потребни систему.",
@@ -114,5 +114,8 @@
"TasksLibraryCategory": "Библиотека",
"TasksMaintenanceCategory": "Одржавање",
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
- "TaskCleanActivityLog": "Очисти историју активности"
+ "TaskCleanActivityLog": "Очисти историју активности",
+ "Undefined": "Недефинисано",
+ "Forced": "Форсирано",
+ "Default": "Подразумевано"
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 447c587f99..f0734340be 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -75,8 +75,7 @@ namespace Emby.Server.Implementations.TV
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
- .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreferenceValues(PreferenceKind.LatestItemExcludes).Contains(i.Id))
.ToArray();
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index f7bb968f0b..87b4577b62 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -85,6 +85,7 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+ dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
// Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 4fd9c2fbf6..694d16ad97 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Description xml returned.
+ /// DLNA is disabled.
/// An containing the description xml.
[HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{
- var url = GetAbsoluteUri();
- var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
- var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
- return Ok(xml);
+ if (DlnaEntryPoint.Enabled)
+ {
+ var url = GetAbsoluteUri();
+ var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
+ var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
+ return Ok(xml);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Dlna content directory returned.
+ /// DLNA is disabled.
/// An containing the dlna content directory xml.
[HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{
- return Ok(_contentDirectory.GetServiceXml());
+ if (DlnaEntryPoint.Enabled)
+ {
+ return Ok(_contentDirectory.GetServiceXml());
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Dlna media receiver registrar xml returned.
+ /// DLNA is disabled.
/// Dlna media receiver registrar xml.
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{
- return Ok(_mediaReceiverRegistrar.GetServiceXml());
+ if (DlnaEntryPoint.Enabled)
+ {
+ return Ok(_mediaReceiverRegistrar.GetServiceXml());
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Dlna media receiver registrar xml returned.
+ /// DLNA is disabled.
/// Dlna media receiver registrar xml.
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{
- return Ok(_connectionManager.GetServiceXml());
+ if (DlnaEntryPoint.Enabled)
+ {
+ return Ok(_connectionManager.GetServiceXml());
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Request processed.
+ /// DLNA is disabled.
/// Control response.
[HttpPost("{serverId}/ContentDirectory/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Request processed.
+ /// DLNA is disabled.
/// Control response.
[HttpPost("{serverId}/ConnectionManager/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Request processed.
+ /// DLNA is disabled.
/// Control response.
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Request processed.
+ /// DLNA is disabled.
/// Event subscription response.
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult ProcessMediaReceiverRegistrarEventRequest(string serverId)
{
- return ProcessEventRequest(_mediaReceiverRegistrar);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return ProcessEventRequest(_mediaReceiverRegistrar);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Request processed.
+ /// DLNA is disabled.
/// Event subscription response.
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult ProcessContentDirectoryEventRequest(string serverId)
{
- return ProcessEventRequest(_contentDirectory);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return ProcessEventRequest(_contentDirectory);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// Request processed.
+ /// DLNA is disabled.
/// Event subscription response.
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult ProcessConnectionManagerEventRequest(string serverId)
{
- return ProcessEventRequest(_connectionManager);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return ProcessEventRequest(_connectionManager);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers
///
/// Server UUID.
/// The icon filename.
+ /// Request processed.
+ /// Not Found.
+ /// DLNA is disabled.
/// Icon stream.
[HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile]
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{
- return GetIconInternal(fileName);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return GetIconInternal(fileName);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
///
@@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers
///
/// The icon filename.
/// Icon stream.
+ /// Request processed.
+ /// Not Found.
+ /// DLNA is disabled.
[HttpGet("icons/{fileName}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile]
public ActionResult GetIcon([FromRoute, Required] string fileName)
{
- return GetIconInternal(fileName);
+ if (DlnaEntryPoint.Enabled)
+ {
+ return GetIconInternal(fileName);
+ }
+
+ return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
private ActionResult GetIconInternal(string fileName)
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index e828a0801b..c606d327c5 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -325,9 +325,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
+ await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -358,9 +360,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
+ await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 7e9035f80f..b84136ac6f 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -254,18 +254,18 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { "Playlist" };
}
- bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
+ var enabledChannels = user!.GetPreferenceValues(PreferenceKind.EnabledChannels);
+
+ bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled
- || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
+ || Array.IndexOf(enabledChannels, item.Id) != -1
// Assume all items inside an EnabledChannel are enabled
- || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
+ || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
- if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
- collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
- StringComparer.OrdinalIgnoreCase))
+ if (user.GetPreferenceValues(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
{
isInEnabledFolder = true;
}
@@ -786,12 +786,12 @@ namespace Jellyfin.Api.Controllers
var ancestorIds = Array.Empty();
- var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
+ var excludeFolderIds = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !excludeFolderIds.Contains(i.Id))
.Select(i => i.Id)
.ToArray();
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 184843b397..28d359ac3a 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
}
// TODO determine non-ASCII validity.
- return PhysicalFile(path, MimeTypes.GetMimeType(path));
+ return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
}
///
@@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers
{
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
- IsMovie = isMovie,
- IsSeries = isSeries,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 56d4b3933e..6f2d432277 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers
///
/// Set channel mappings.
///
- /// Provider id.
- /// Tuner channel id.
- /// Provider channel id.
+ /// The set channel mapping dto.
/// Created channel mapping returned.
/// An containing the created channel mapping.
[HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SetChannelMapping(
- [FromQuery] string? providerId,
- [FromQuery] string? tunerChannelId,
- [FromQuery] string? providerChannelId)
+ public async Task> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
{
- return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false);
+ return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
}
///
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 2a1da31c9e..baa2e06361 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -259,24 +259,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels,
[FromQuery] Guid? itemId,
- [FromBody] OpenLiveStreamDto openLiveStreamDto,
- [FromQuery] bool enableDirectPlay = true,
- [FromQuery] bool enableDirectStream = true)
+ [FromBody] OpenLiveStreamDto? openLiveStreamDto,
+ [FromQuery] bool? enableDirectPlay,
+ [FromQuery] bool? enableDirectStream)
{
var request = new LiveStreamRequest
{
- OpenToken = openToken,
- UserId = userId ?? Guid.Empty,
- PlaySessionId = playSessionId,
- MaxStreamingBitrate = maxStreamingBitrate,
- StartTimeTicks = startTimeTicks,
- AudioStreamIndex = audioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex,
- MaxAudioChannels = maxAudioChannels,
- ItemId = itemId ?? Guid.Empty,
+ OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
+ UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
+ PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
+ MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
+ StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
+ AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
+ MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
+ ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
DeviceProfile = openLiveStreamDto?.DeviceProfile,
- EnableDirectPlay = enableDirectPlay,
- EnableDirectStream = enableDirectStream,
+ EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
+ EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
};
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 7784e8a119..d79bea9855 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
+using System.Net;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
@@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetSystemInfo()
{
- return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
+ return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
///
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetPublicSystemInfo()
{
- return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
+ return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
///
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index b4f2817f71..f01f50cea7 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -15,9 +15,7 @@
-
-
diff --git a/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
new file mode 100644
index 0000000000..2ddaa89e8b
--- /dev/null
+++ b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.LiveTvDtos
+{
+ ///
+ /// Set channel mapping dto.
+ ///
+ public class SetChannelMappingDto
+ {
+ ///
+ /// Gets or sets the provider id.
+ ///
+ [Required]
+ public string ProviderId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the tuner channel id.
+ ///
+ [Required]
+ public string TunerChannelId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the provider channel id.
+ ///
+ [Required]
+ public string ProviderChannelId { get; set; } = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
index b0b3de8553..7045423261 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -10,6 +10,61 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
///
public class OpenLiveStreamDto
{
+ ///
+ /// Gets or sets the open token.
+ ///
+ public string? OpenToken { get; set; }
+
+ ///
+ /// Gets or sets the user id.
+ ///
+ public Guid? UserId { get; set; }
+
+ ///
+ /// Gets or sets the play session id.
+ ///
+ public string? PlaySessionId { get; set; }
+
+ ///
+ /// Gets or sets the max streaming bitrate.
+ ///
+ public int? MaxStreamingBitrate { get; set; }
+
+ ///
+ /// Gets or sets the start time in ticks.
+ ///
+ public long? StartTimeTicks { get; set; }
+
+ ///
+ /// Gets or sets the audio stream index.
+ ///
+ public int? AudioStreamIndex { get; set; }
+
+ ///
+ /// Gets or sets the subtitle stream index.
+ ///
+ public int? SubtitleStreamIndex { get; set; }
+
+ ///
+ /// Gets or sets the max audio channels.
+ ///
+ public int? MaxAudioChannels { get; set; }
+
+ ///
+ /// Gets or sets the item id.
+ ///
+ public Guid? ItemId { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to enable direct play.
+ ///
+ public bool? EnableDirectPlay { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to enale direct stream.
+ ///
+ public bool? EnableDirectStream { get; set; }
+
///
/// Gets or sets the device profile.
///
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 0fd8cb224f..362f3b4ebb 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -413,6 +413,44 @@ namespace Jellyfin.Data.Entities
return Equals(val, string.Empty) ? Array.Empty() : val.Split(Delimiter);
}
+ ///
+ /// Gets the user's preferences for the given preference kind.
+ ///
+ /// The preference kind.
+ /// Type of preference.
+ /// A {T} array containing the user's preference.
+ public T[] GetPreferenceValues(PreferenceKind preference)
+ {
+ var val = Preferences.First(p => p.Kind == preference).Value;
+ if (string.IsNullOrEmpty(val))
+ {
+ return Array.Empty();
+ }
+
+ // Convert array of {string} to array of {T}
+ var converter = TypeDescriptor.GetConverter(typeof(T));
+ var stringValues = val.Split(Delimiter);
+ var convertedCount = 0;
+ var parsedValues = new T[stringValues.Length];
+ for (var i = 0; i < stringValues.Length; i++)
+ {
+ try
+ {
+ var parsedValue = converter.ConvertFromString(stringValues[i].Trim());
+ if (parsedValue != null)
+ {
+ parsedValues[convertedCount++] = (T)parsedValue;
+ }
+ }
+ catch (FormatException)
+ {
+ // Unable to convert value
+ }
+ }
+
+ return parsedValues[..convertedCount];
+ }
+
///
/// Sets the specified preference to the given value.
///
@@ -421,7 +459,19 @@ namespace Jellyfin.Data.Entities
public void SetPreference(PreferenceKind preference, string[] values)
{
Preferences.First(p => p.Kind == preference).Value
- = string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values);
+ = string.Join(Delimiter, values);
+ }
+
+ ///
+ /// Sets the specified preference to the given value.
+ ///
+ /// The preference kind.
+ /// The values.
+ /// The type of value.
+ public void SetPreference(PreferenceKind preference, T[] values)
+ {
+ Preferences.First(p => p.Kind == preference).Value
+ = string.Join(Delimiter, values);
}
///
@@ -441,7 +491,7 @@ namespace Jellyfin.Data.Entities
/// True if the folder is in the user's grouped folders.
public bool IsFolderGrouped(Guid id)
{
- return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id);
+ return Array.IndexOf(GetPreferenceValues(PreferenceKind.GroupedFolders), id) != -1;
}
private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index b76b272cfc..d1de5408c4 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -375,14 +375,14 @@ namespace Jellyfin.Server.Implementations.Users
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
- EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(),
+ EnabledChannels = user.GetPreferenceValues(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
- EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(),
+ EnabledFolders = user.GetPreferenceValues(PreferenceKind.EnabledFolders),
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
SyncPlayAccess = user.SyncPlayAccess,
- BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(),
- BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(),
- BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse).ToArray()
+ BlockedChannels = user.GetPreferenceValues(PreferenceKind.BlockedChannels),
+ BlockedMediaFolders = user.GetPreferenceValues(PreferenceKind.BlockedMediaFolders),
+ BlockUnratedItems = user.GetPreferenceValues(PreferenceKind.BlockUnratedItems)
}
};
}
@@ -703,13 +703,11 @@ namespace Jellyfin.Server.Implementations.Users
}
// TODO: fix this at some point
- user.SetPreference(
- PreferenceKind.BlockUnratedItems,
- policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty());
+ user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index dd005b7f47..f4040748d7 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -8,7 +8,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -81,7 +80,8 @@ namespace Jellyfin.Server.Migrations.Routines
{ "unstable", ChromecastVersion.Unstable }
};
- var customDisplayPrefs = new HashSet();
+ var displayPrefs = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var customDisplayPrefs = new HashSet(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
{
@@ -98,6 +98,15 @@ namespace Jellyfin.Server.Migrations.Routines
var itemId = new Guid(result[1].ToBlob());
var dtoUserId = new Guid(result[1].ToBlob());
+ var client = result[2].ToString();
+ var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
+ if (displayPrefs.Contains(displayPreferencesKey))
+ {
+ // Duplicate display preference.
+ continue;
+ }
+
+ displayPrefs.Add(displayPreferencesKey);
var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null)
{
@@ -110,7 +119,7 @@ namespace Jellyfin.Server.Migrations.Routines
: ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion");
- var displayPreferences = new DisplayPreferences(dtoUserId, itemId, result[2].ToString())
+ var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{
IndexBy = Enum.TryParse(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop,
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 74c5503314..33f039c394 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -168,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
}
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
diff --git a/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs
new file mode 100644
index 0000000000..73e3a0493d
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ ///
+ /// Legacy DateTime converter.
+ /// Milliseconds aren't output if zero by default.
+ ///
+ public class JsonDateTimeConverter : JsonConverter
+ {
+ ///
+ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.GetDateTime();
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
+ {
+ if (value.Millisecond == 0)
+ {
+ // Remaining ticks value will be 0, manually format.
+ writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ writer.WriteStringValue(value);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index f232ebdc7f..2b5f04cf82 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -19,56 +19,71 @@ namespace MediaBrowser.Common.Json
///
public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
+ ///
+ /// When changing these options, update
+ /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+ /// -> AddJellyfinApi
+ /// -> AddJsonOptions.
+ ///
+ private static readonly JsonSerializerOptions _jsonSerializerOptions = new ()
+ {
+ ReadCommentHandling = JsonCommentHandling.Disallow,
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+
+ Converters =
+ {
+ new JsonGuidConverter(),
+ new JsonVersionConverter(),
+ new JsonStringEnumConverter(),
+ new JsonNullableStructConverterFactory(),
+ new JsonBoolNumberConverter(),
+ new JsonDateTimeConverter()
+ }
+ };
+
+ private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions)
+ {
+ PropertyNamingPolicy = null
+ };
+
+ private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions)
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
///
/// Gets the default options.
///
///
- /// When changing these options, update
- /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
- /// -> AddJellyfinApi
- /// -> AddJsonOptions.
+ /// The return value must not be modified.
+ /// If the defaults must be modified the author must use the copy constructor.
///
/// The default options.
public static JsonSerializerOptions GetOptions()
- {
- var options = new JsonSerializerOptions
- {
- ReadCommentHandling = JsonCommentHandling.Disallow,
- WriteIndented = false,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- NumberHandling = JsonNumberHandling.AllowReadingFromString,
- PropertyNameCaseInsensitive = true
- };
-
- options.Converters.Add(new JsonGuidConverter());
- options.Converters.Add(new JsonVersionConverter());
- options.Converters.Add(new JsonStringEnumConverter());
- options.Converters.Add(new JsonNullableStructConverterFactory());
- options.Converters.Add(new JsonBoolNumberConverter());
-
- return options;
- }
+ => _jsonSerializerOptions;
///
/// Gets camelCase json options.
///
+ ///
+ /// The return value must not be modified.
+ /// If the defaults must be modified the author must use the copy constructor.
+ ///
/// The camelCase options.
public static JsonSerializerOptions GetCamelCaseOptions()
- {
- var options = GetOptions();
- options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
- return options;
- }
+ => _camelCaseJsonSerializerOptions;
///
/// Gets PascalCase json options.
///
+ ///
+ /// The return value must not be modified.
+ /// If the defaults must be modified the author must use the copy constructor.
+ ///
/// The PascalCase options.
public static JsonSerializerOptions GetPascalCaseOptions()
- {
- var options = GetOptions();
- options.PropertyNamingPolicy = null;
- return options;
- }
+ => _pascalCaseJsonSerializerOptions;
}
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index be5e7f5b45..320e60dc66 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -14,6 +14,7 @@
+
@@ -21,7 +22,6 @@
-
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index 129cdb519f..b2315bda47 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -17,9 +17,10 @@ namespace MediaBrowser.Controller.Channels
{
public override bool IsVisible(User user)
{
- if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
+ var blockedChannelsPreference = user.GetPreferenceValues(PreferenceKind.BlockedChannels);
+ if (blockedChannelsPreference.Length != 0)
{
- if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (blockedChannelsPreference.Contains(Id))
{
return false;
}
@@ -27,8 +28,7 @@ namespace MediaBrowser.Controller.Channels
else
{
if (!user.HasPermission(PermissionKind.EnableAllChannels)
- && !user.GetPreference(PreferenceKind.EnabledChannels)
- .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ && !user.GetPreferenceValues(PreferenceKind.EnabledChannels).Contains(Id))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 48cd9371a0..9a33ad9d74 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
+ return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index c5e50cf45d..8a9bb12c7b 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
+ return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 1b25fbdbb7..cbb02aabd7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -480,11 +480,11 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders);
+ var allowed = user.GetPreferenceValues(PreferenceKind.EnableContentDeletionFromFolders);
if (SourceType == SourceType.Channel)
{
- return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase);
+ return allowed.Contains(ChannelId);
}
else
{
@@ -492,7 +492,7 @@ namespace MediaBrowser.Controller.Entities
foreach (var folder in collectionFolders)
{
- if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (allowed.Contains(folder.Id))
{
return true;
}
@@ -1909,7 +1909,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString());
+ return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType());
}
///
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 57d04ddfac..cac5026f70 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -186,13 +186,10 @@ namespace MediaBrowser.Controller.Entities
{
if (this is ICollectionFolder && !(this is BasePluginFolder))
{
- var blockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders);
+ var blockedMediaFolders = user.GetPreferenceValues(PreferenceKind.BlockedMediaFolders);
if (blockedMediaFolders.Length > 0)
{
- if (blockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) ||
-
- // Backwards compatibility
- blockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+ if (blockedMediaFolders.Contains(Id))
{
return false;
}
@@ -200,8 +197,7 @@ namespace MediaBrowser.Controller.Entities
else
{
if (!user.HasPermission(PermissionKind.EnableAllFolders)
- && !user.GetPreference(PreferenceKind.EnabledFolders)
- .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ && !user.GetPreferenceValues(PreferenceKind.EnabledFolders).Contains(Id))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 8de88cc1b1..05e4229cad 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Controller.Entities.Movies
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie.ToString());
+ return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
}
public override double GetDefaultPrimaryImageAspectRatio()
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index e8afa9a490..1a379074dc 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities.TV
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString());
+ return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 91a03e6472..73ede7c5a4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1127,13 +1127,25 @@ namespace MediaBrowser.Controller.MediaEncoding
targetVideoCodec = "hevc";
}
- var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
- profile = Regex.Replace(profile, @"\s+", String.Empty);
+ var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
+ profile = Regex.Replace(profile, @"\s+", string.Empty);
+
+ // We only transcode to HEVC 8-bit for now, force Main Profile.
+ if (profile.Contains("main 10", StringComparison.OrdinalIgnoreCase)
+ || profile.Contains("main still", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
+
+ // Extended Profile is not supported by any known h264 encoders, force Main Profile.
+ if (profile.Contains("extended", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
// Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("high 10", StringComparison.OrdinalIgnoreCase))
{
profile = "high";
}
@@ -1141,8 +1153,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_baseline";
}
@@ -1151,16 +1162,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
- && profile != null
- && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "baseline";
}
+ // libx264, h264_qsv, h264_nvenc and h264_vaapi does not support Constrained High profile, force High in this case.
+ if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ && profile.Contains("high", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "high";
+ }
+
// Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
- if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1)
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("main 10", StringComparison.OrdinalIgnoreCase))
{
profile = "main";
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 7bb2a7d03f..f8af499e4c 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index 1782b42e21..98097477c6 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Entities
}
instance.ProviderIds.TryGetValue(name, out string? id);
- return id;
+ return string.IsNullOrEmpty(id) ? null : id;
}
///
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index b86187f9be..334fe82097 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -33,7 +33,6 @@
-
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index accdea36e4..fc8eb8c4e4 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -22,7 +22,6 @@
-
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index c8fc568a22..9679081975 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,10 +1,16 @@
#pragma warning disable CS1591
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -13,14 +19,27 @@ namespace MediaBrowser.Providers.TV
{
public class SeriesMetadataService : MetadataService
{
+ private readonly ILocalizationManager _localizationManager;
+
public SeriesMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger logger,
IProviderManager providerManager,
IFileSystem fileSystem,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ ILocalizationManager localizationManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
+ _localizationManager = localizationManager;
+ }
+
+ ///
+ protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+ {
+ await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+
+ RemoveObsoleteSeasons(item);
+ await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
///
@@ -62,5 +81,117 @@ namespace MediaBrowser.Providers.TV
targetItem.AirDays = sourceItem.AirDays;
}
}
+
+ private void RemoveObsoleteSeasons(Series series)
+ {
+ // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync.
+ var physicalSeasonNumbers = new HashSet();
+ var virtualSeasons = new List();
+ foreach (var existingSeason in series.Children.OfType())
+ {
+ if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
+ {
+ physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
+ }
+ else if (existingSeason.LocationType == LocationType.Virtual)
+ {
+ virtualSeasons.Add(existingSeason);
+ }
+ }
+
+ foreach (var virtualSeason in virtualSeasons)
+ {
+ var seasonNumber = virtualSeason.IndexNumber;
+ // If there's a physical season with the same number or no episodes in the season, delete it
+ if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
+ || !virtualSeason.GetEpisodes().Any())
+ {
+ Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
+
+ LibraryManager.DeleteItem(
+ virtualSeason,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ false);
+ }
+ }
+ }
+
+ ///
+ /// Creates seasons for all episodes that aren't in a season folder.
+ /// If no season number can be determined, a dummy season will be created.
+ ///
+ /// The series.
+ /// The cancellation token.
+ /// The async task.
+ private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
+ {
+ var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
+ .Cast()
+ .Where(i => !i.IsInSeasonFolder);
+
+ List seasons = series.Children.OfType().ToList();
+
+ // Loop through the unique season numbers
+ foreach (var episode in episodesInSeriesFolder)
+ {
+ // Null season numbers will have a 'dummy' season created because seasons are always required.
+ var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null;
+ var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
+
+ if (existingSeason == null)
+ {
+ var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false);
+ seasons.Add(season);
+ }
+ else if (existingSeason.IsVirtualItem)
+ {
+ existingSeason.IsVirtualItem = false;
+ await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+ ///
+ /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
+ ///
+ /// The series.
+ /// The season number.
+ /// The cancellation token.
+ /// The newly created season.
+ private async Task CreateSeasonAsync(
+ Series series,
+ int? seasonNumber,
+ CancellationToken cancellationToken)
+ {
+ string seasonName = seasonNumber switch
+ {
+ null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
+ 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
+ _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
+ };
+
+ Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
+
+ var season = new Season
+ {
+ Name = seasonName,
+ IndexNumber = seasonNumber,
+ Id = LibraryManager.GetNewItemId(
+ series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
+ typeof(Season)),
+ IsVirtualItem = false,
+ SeriesId = series.Id,
+ SeriesName = series.Name
+ };
+
+ series.AddChild(season, cancellationToken);
+
+ await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
+
+ return season;
+ }
}
}
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
new file mode 100644
index 0000000000..08b4ffa528
--- /dev/null
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -0,0 +1,31 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=5.0
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64-musl /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
new file mode 100644
index 0000000000..b8499c9172
--- /dev/null
+++ b/deployment/Dockerfile.linux.arm64
@@ -0,0 +1,31 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=5.0
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=arm64
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.arm64 /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
new file mode 100644
index 0000000000..80c4d14693
--- /dev/null
+++ b/deployment/Dockerfile.linux.armhf
@@ -0,0 +1,31 @@
+FROM debian:10
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=5.0
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=armhf
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.armhf /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/build.linux.amd64-musl b/deployment/build.linux.amd64-musl
new file mode 100755
index 0000000000..72444c05e7
--- /dev/null
+++ b/deployment/build.linux.amd64-musl
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux amd64-musl .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-amd64-musl.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64
new file mode 100755
index 0000000000..e362607a76
--- /dev/null
+++ b/deployment/build.linux.arm64
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux arm64 .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-arm64.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf
new file mode 100755
index 0000000000..c0d0607fc7
--- /dev/null
+++ b/deployment/build.linux.armhf
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux armhf .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-armhf.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd