Updated to latest unstable.

This commit is contained in:
BaronGreenback 2020-12-23 17:25:34 +00:00
commit 889e988167
56 changed files with 828 additions and 191 deletions

View File

@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf BuildConfiguration: ubuntu.armhf
Linux.amd64: Linux.amd64:
BuildConfiguration: linux.amd64 BuildConfiguration: linux.amd64
Linux.amd64-musl:
BuildConfiguration: linux.amd64-musl
Linux.arm64:
BuildConfiguration: linux.arm64
Linux.armhf:
BuildConfiguration: linux.armhf
Windows.amd64: Windows.amd64:
BuildConfiguration: windows.amd64 BuildConfiguration: windows.amd64
MacOS: MacOS:

View File

@ -78,9 +78,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -126,6 +126,11 @@ namespace Emby.Dlna.Main
public static DlnaEntryPoint Current { get; private set; } public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; } public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; } public IConnectionManager ConnectionManager { get; private set; }
@ -152,6 +157,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents() private void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer;
StartSsdpHandler(); StartSsdpHandler();

View File

@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts", Container = "ts,mpegts",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264", VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac" AudioCodec = "aac,ac3,mp2"
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "ts", Container = "ts",
VideoCodec = "h264", VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3", AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new TranscodingProfile new TranscodingProfile

View File

@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts", Container = "ts,mpegts",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264", VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac" AudioCodec = "aac,ac3,mp2"
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "ts", Container = "ts",
VideoCodec = "h264", VideoCodec = "h264",
AudioCodec = "mp3", AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new TranscodingProfile new TranscodingProfile

View File

@ -38,7 +38,7 @@
<XmlRootAttributes /> <XmlRootAttributes />
<DirectPlayProfiles> <DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@ -46,7 +46,7 @@
</DirectPlayProfiles> </DirectPlayProfiles>
<TranscodingProfiles> <TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles> </TranscodingProfiles>
<ContainerProfiles> <ContainerProfiles>

View File

@ -38,7 +38,7 @@
<XmlRootAttributes /> <XmlRootAttributes />
<DirectPlayProfiles> <DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@ -46,7 +46,7 @@
</DirectPlayProfiles> </DirectPlayProfiles>
<TranscodingProfiles> <TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles> </TranscodingProfiles>
<ContainerProfiles> <ContainerProfiles>

View File

@ -23,14 +23,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />

View File

@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext) public User GetUser(object requestContext)
{ {
return GetUser((HttpContext)requestContext); return GetUser(((HttpRequest)requestContext).HttpContext);
} }
} }
} }

View File

@ -129,23 +129,23 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden) 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<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
} }
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList(); var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list return list
.OrderBy(i => .OrderBy(i =>
{ {
var index = orders.IndexOf(i.Id.ToString("D", CultureInfo.InvariantCulture)); var index = Array.IndexOf(orders, i.Id);
if (index == -1 if (index == -1
&& i is UserView view && i is UserView view
&& view.DisplayParentId != Guid.Empty) && 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; return index == -1 ? int.MaxValue : index;
@ -280,8 +280,8 @@ namespace Emby.Server.Implementations.Library
{ {
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder) .Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .Contains(i.Id))
.ToList(); .ToList();
} }

View File

@ -784,18 +784,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var allStations = root.stations ?? new List<ScheduleDirect.Station>(); var allStations = root.stations ?? new List<ScheduleDirect.Station>();
var map = root.map; var map = root.map;
int len = map.Count; var list = new List<ChannelInfo>(map.Count);
var array = new List<ChannelInfo>(len); foreach (var channel in map)
for (int i = 0; i < len; i++)
{ {
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) if (station == null)
{ {
station = new ScheduleDirect.Station 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; channelInfo.ImageUrl = station.logo.URL;
} }
array[i] = channelInfo; list.Add(channelInfo);
} }
return array; return list;
} }
private static string NormalizeName(string value) private static string NormalizeName(string value)

View File

@ -115,5 +115,8 @@
"TasksLibraryCategory": "Library", "TasksLibraryCategory": "Library",
"TasksMaintenanceCategory": "Maintenance", "TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.", "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanActivityLog": "Clean Activity Log" "TaskCleanActivityLog": "Clean Activity Log",
"Undefined": "Undefined",
"Forced": "Forced",
"Default": "Default"
} }

View File

@ -112,5 +112,9 @@
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}", "CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito", "AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación", "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"
} }

View File

@ -4,7 +4,7 @@
"Application": "アプリケーション", "Application": "アプリケーション",
"Artists": "アーティスト", "Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました", "AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
"Books": "ブック", "Books": "ブック",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました", "CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル", "Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}", "ChapterNameValue": "チャプター {0}",
@ -114,5 +114,8 @@
"TaskRefreshChapterImages": "チャプター画像を抽出する", "TaskRefreshChapterImages": "チャプター画像を抽出する",
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする", "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。", "TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
"TaskCleanActivityLog": "アクティビティの履歴を消去" "TaskCleanActivityLog": "アクティビティの履歴を消去",
"Undefined": "未定義",
"Forced": "強制",
"Default": "デフォルト"
} }

View File

@ -91,5 +91,22 @@
"UserStoppedPlayingItemWithValues": "{0} - {1} oınatýyn {2} toqtatty", "UserStoppedPlayingItemWithValues": "{0} - {1} oınatýyn {2} toqtatty",
"ValueHasBeenAddedToLibrary": "{0} (tasyǵyshhanaǵa ústelindi)", "ValueHasBeenAddedToLibrary": "{0} (tasyǵyshhanaǵa ústelindi)",
"ValueSpecialEpisodeName": "Arnaıy - {0}", "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"
} }

View File

@ -1,7 +1,7 @@
{ {
"Albums": "Albums", "Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}", "AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Programma", "Application": "Applicatie",
"Artists": "Artiesten", "Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd", "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
"Books": "Boeken", "Books": "Boeken",

View File

@ -96,7 +96,7 @@
"TaskRefreshChannels": "Обновление каналов", "TaskRefreshChannels": "Обновление каналов",
"TaskCleanTranscode": "Очистка каталога перекодировки", "TaskCleanTranscode": "Очистка каталога перекодировки",
"TaskUpdatePlugins": "Обновление плагинов", "TaskUpdatePlugins": "Обновление плагинов",
"TaskRefreshPeople": "Обновление метаданных людей", "TaskRefreshPeople": "Подновить людей",
"TaskCleanLogs": "Очистка каталога журналов", "TaskCleanLogs": "Очистка каталога журналов",
"TaskRefreshLibrary": "Сканирование медиатеки", "TaskRefreshLibrary": "Сканирование медиатеки",
"TaskRefreshChapterImages": "Извлечение изображений сцен", "TaskRefreshChapterImages": "Извлечение изображений сцен",
@ -109,7 +109,7 @@
"TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.", "TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.",
"TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.", "TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.",
"TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.", "TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.",
"TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.", "TaskRefreshPeopleDescription": "Обновляются метаданные для актёров и режиссёров в медиатеке.",
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).", "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.", "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",

View File

@ -66,7 +66,7 @@
"Inherit": "Наследи", "Inherit": "Наследи",
"HomeVideos": "Кућни видео", "HomeVideos": "Кућни видео",
"HeaderRecordingGroups": "Групе снимања", "HeaderRecordingGroups": "Групе снимања",
"HeaderNextUp": "Следеће горе", "HeaderNextUp": "Следи",
"HeaderLiveTV": "ТВ уживо", "HeaderLiveTV": "ТВ уживо",
"HeaderFavoriteSongs": "Омиљене песме", "HeaderFavoriteSongs": "Омиљене песме",
"HeaderFavoriteShows": "Омиљене серије", "HeaderFavoriteShows": "Омиљене серије",
@ -79,17 +79,17 @@
"Folders": "Фасцикле", "Folders": "Фасцикле",
"Favorites": "Омиљено", "Favorites": "Омиљено",
"FailedLoginAttemptWithUserName": "Неуспела пријава са {0}", "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
"DeviceOnlineWithName": "{0} се повезао", "DeviceOnlineWithName": "{0} је повезан",
"DeviceOfflineWithName": "{0} је прекинуо везу", "DeviceOfflineWithName": "{0} је прекинуо везу",
"Collections": "Колекције", "Collections": "Колекције",
"ChapterNameValue": "Поглавље {0}", "ChapterNameValue": "Поглавље {0}",
"Channels": "Канали", "Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографија је послата са {0}", "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге", "Books": "Књиге",
"AuthenticationSucceededWithUserName": "{0} успешно проверено", "AuthenticationSucceededWithUserName": "{0} успешно проверено",
"Artists": "Извођач", "Artists": "Извођачи",
"Application": "Апликација", "Application": "Апликација",
"AppDeviceValues": "Апл: {0}, уређај: {1}", "AppDeviceValues": "Апликација: {0}, Уређај: {1}",
"Albums": "Албуми", "Albums": "Албуми",
"TaskDownloadMissingSubtitlesDescription": "Претражује интернет за недостајуће титлове на основу конфигурације метаподатака.", "TaskDownloadMissingSubtitlesDescription": "Претражује интернет за недостајуће титлове на основу конфигурације метаподатака.",
"TaskDownloadMissingSubtitles": "Преузмите недостајуће титлове", "TaskDownloadMissingSubtitles": "Преузмите недостајуће титлове",
@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Брише логове старије од {0} дана.", "TaskCleanLogsDescription": "Брише логове старије од {0} дана.",
"TaskCleanLogs": "Очистите директоријум логова", "TaskCleanLogs": "Очистите директоријум логова",
"TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.", "TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.",
"TaskRefreshLibrary": "Скенирај Библиотеку Медија", "TaskRefreshLibrary": "Скенирај библиотеку медија",
"TaskRefreshChapterImagesDescription": "Ствара сличице за видео записе који имају поглавља.", "TaskRefreshChapterImagesDescription": "Ствара сличице за видео записе који имају поглавља.",
"TaskRefreshChapterImages": "Издвоји слике из поглавља", "TaskRefreshChapterImages": "Издвоји слике из поглавља",
"TaskCleanCacheDescription": "Брише Кеш фајлове који више нису потребни систему.", "TaskCleanCacheDescription": "Брише Кеш фајлове који више нису потребни систему.",
@ -114,5 +114,8 @@
"TasksLibraryCategory": "Библиотека", "TasksLibraryCategory": "Библиотека",
"TasksMaintenanceCategory": "Одржавање", "TasksMaintenanceCategory": "Одржавање",
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.", "TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
"TaskCleanActivityLog": "Очисти историју активности" "TaskCleanActivityLog": "Очисти историју активности",
"Undefined": "Недефинисано",
"Forced": "Форсирано",
"Default": "Подразумевано"
} }

View File

@ -75,8 +75,7 @@ namespace Emby.Server.Implementations.TV
{ {
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder) .Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes).Contains(i.Id))
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToArray(); .ToArray();
} }

View File

@ -85,6 +85,7 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
// Load all custom display preferences // Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);

View File

@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Description xml returned.</response> /// <response code="200">Description xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns> /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")] [HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{ {
var url = GetAbsoluteUri(); if (DlnaEntryPoint.Enabled)
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); {
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); var url = GetAbsoluteUri();
return Ok(xml); 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);
} }
/// <summary> /// <summary>
@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna content directory returned.</response> /// <response code="200">Dlna content directory returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory")] [HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId) public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{ {
return Ok(_contentDirectory.GetServiceXml()); if (DlnaEntryPoint.Enabled)
{
return Ok(_contentDirectory.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response> /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{ {
return Ok(_mediaReceiverRegistrar.GetServiceXml()); if (DlnaEntryPoint.Enabled)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response> /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId) public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{ {
return Ok(_connectionManager.GetServiceXml()); if (DlnaEntryPoint.Enabled)
{
return Ok(_connectionManager.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")] [HttpPost("{serverId}/ContentDirectory/Control")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> 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);
} }
/// <summary> /// <summary>
@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")] [HttpPost("{serverId}/ConnectionManager/Control")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> 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);
} }
/// <summary> /// <summary>
@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")] [HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> 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);
} }
/// <summary> /// <summary>
@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{ {
return ProcessEventRequest(_mediaReceiverRegistrar); if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ContentDirectory/Events")] [HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")] [HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{ {
return ProcessEventRequest(_contentDirectory); if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_contentDirectory);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ConnectionManager/Events")] [HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")] [HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{ {
return ProcessEventRequest(_connectionManager); if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_connectionManager);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <param name="fileName">The icon filename.</param> /// <param name="fileName">The icon filename.</param>
/// <response code="200">Request processed.</response>
/// <response code="404">Not Found.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Icon stream.</returns> /// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")] [HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{ {
return GetIconInternal(fileName); if (DlnaEntryPoint.Enabled)
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="fileName">The icon filename.</param> /// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns> /// <returns>Icon stream.</returns>
/// <response code="200">Request processed.</response>
/// <response code="404">Not Found.</response>
/// <response code="503">DLNA is disabled.</response>
[HttpGet("icons/{fileName}")] [HttpGet("icons/{fileName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetIcon([FromRoute, Required] string fileName) 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) private ActionResult GetIconInternal(string fileName)

View File

@ -325,9 +325,11 @@ namespace Jellyfin.Api.Controllers
return NotFound(); return NotFound();
} }
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault(); 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); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
@ -358,9 +360,11 @@ namespace Jellyfin.Api.Controllers
return NotFound(); return NotFound();
} }
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault(); 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); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();

View File

@ -254,18 +254,18 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { "Playlist" }; includeItemTypes = new[] { "Playlist" };
} }
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled // 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 // 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); var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders) foreach (var collectionFolder in collectionFolders)
{ {
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains( if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
StringComparer.OrdinalIgnoreCase))
{ {
isInEnabledFolder = true; isInEnabledFolder = true;
} }
@ -786,12 +786,12 @@ namespace Jellyfin.Api.Controllers
var ancestorIds = Array.Empty<Guid>(); var ancestorIds = Array.Empty<Guid>();
var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes); var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{ {
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder) .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) .Select(i => i.Id)
.ToArray(); .ToArray();
} }

View File

@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
} }
// TODO determine non-ASCII validity. // TODO determine non-ASCII validity.
return PhysicalFile(path, MimeTypes.GetMimeType(path)); return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
} }
/// <summary> /// <summary>
@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers
{ {
Limit = limit, Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(), IncludeItemTypes = includeItemTypes.ToArray(),
IsMovie = isMovie,
IsSeries = isSeries,
SimilarTo = item, SimilarTo = item,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true, EnableTotalRecordCount = !isMovie ?? true,

View File

@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Set channel mappings. /// Set channel mappings.
/// </summary> /// </summary>
/// <param name="providerId">Provider id.</param> /// <param name="setChannelMappingDto">The set channel mapping dto.</param>
/// <param name="tunerChannelId">Tuner channel id.</param>
/// <param name="providerChannelId">Provider channel id.</param>
/// <response code="200">Created channel mapping returned.</response> /// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")] [HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping( public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
[FromQuery] string? providerId,
[FromQuery] string? tunerChannelId,
[FromQuery] string? providerChannelId)
{ {
return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false); return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@ -259,24 +259,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
[FromQuery] Guid? itemId, [FromQuery] Guid? itemId,
[FromBody] OpenLiveStreamDto openLiveStreamDto, [FromBody] OpenLiveStreamDto? openLiveStreamDto,
[FromQuery] bool enableDirectPlay = true, [FromQuery] bool? enableDirectPlay,
[FromQuery] bool enableDirectStream = true) [FromQuery] bool? enableDirectStream)
{ {
var request = new LiveStreamRequest var request = new LiveStreamRequest
{ {
OpenToken = openToken, OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
UserId = userId ?? Guid.Empty, UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
PlaySessionId = playSessionId, PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
MaxStreamingBitrate = maxStreamingBitrate, MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
ItemId = itemId ?? Guid.Empty, ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
DeviceProfile = openLiveStreamDto?.DeviceProfile, DeviceProfile = openLiveStreamDto?.DeviceProfile,
EnableDirectPlay = enableDirectPlay, EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
EnableDirectStream = enableDirectStream, EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
}; };
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Mime; using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo() public ActionResult<SystemInfo> GetSystemInfo()
{ {
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
} }
/// <summary> /// <summary>
@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo() public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{ {
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
} }
/// <summary> /// <summary>

View File

@ -15,9 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />

View File

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.LiveTvDtos
{
/// <summary>
/// Set channel mapping dto.
/// </summary>
public class SetChannelMappingDto
{
/// <summary>
/// Gets or sets the provider id.
/// </summary>
[Required]
public string ProviderId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the tuner channel id.
/// </summary>
[Required]
public string TunerChannelId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the provider channel id.
/// </summary>
[Required]
public string ProviderChannelId { get; set; } = string.Empty;
}
}

View File

@ -10,6 +10,61 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
/// </summary> /// </summary>
public class OpenLiveStreamDto public class OpenLiveStreamDto
{ {
/// <summary>
/// Gets or sets the open token.
/// </summary>
public string? OpenToken { get; set; }
/// <summary>
/// Gets or sets the user id.
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the play session id.
/// </summary>
public string? PlaySessionId { get; set; }
/// <summary>
/// Gets or sets the max streaming bitrate.
/// </summary>
public int? MaxStreamingBitrate { get; set; }
/// <summary>
/// Gets or sets the start time in ticks.
/// </summary>
public long? StartTimeTicks { get; set; }
/// <summary>
/// Gets or sets the audio stream index.
/// </summary>
public int? AudioStreamIndex { get; set; }
/// <summary>
/// Gets or sets the subtitle stream index.
/// </summary>
public int? SubtitleStreamIndex { get; set; }
/// <summary>
/// Gets or sets the max audio channels.
/// </summary>
public int? MaxAudioChannels { get; set; }
/// <summary>
/// Gets or sets the item id.
/// </summary>
public Guid? ItemId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable direct play.
/// </summary>
public bool? EnableDirectPlay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enale direct stream.
/// </summary>
public bool? EnableDirectStream { get; set; }
/// <summary> /// <summary>
/// Gets or sets the device profile. /// Gets or sets the device profile.
/// </summary> /// </summary>

View File

@ -2,9 +2,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -413,6 +413,44 @@ namespace Jellyfin.Data.Entities
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter); return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
} }
/// <summary>
/// Gets the user's preferences for the given preference kind.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <typeparam name="T">Type of preference.</typeparam>
/// <returns>A {T} array containing the user's preference.</returns>
public T[] GetPreferenceValues<T>(PreferenceKind preference)
{
var val = Preferences.First(p => p.Kind == preference).Value;
if (string.IsNullOrEmpty(val))
{
return Array.Empty<T>();
}
// 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];
}
/// <summary> /// <summary>
/// Sets the specified preference to the given value. /// Sets the specified preference to the given value.
/// </summary> /// </summary>
@ -421,7 +459,19 @@ namespace Jellyfin.Data.Entities
public void SetPreference(PreferenceKind preference, string[] values) public void SetPreference(PreferenceKind preference, string[] values)
{ {
Preferences.First(p => p.Kind == preference).Value Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values); = string.Join(Delimiter, values);
}
/// <summary>
/// Sets the specified preference to the given value.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <param name="values">The values.</param>
/// <typeparam name="T">The type of value.</typeparam>
public void SetPreference<T>(PreferenceKind preference, T[] values)
{
Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter, values);
} }
/// <summary> /// <summary>
@ -441,7 +491,7 @@ namespace Jellyfin.Data.Entities
/// <returns><c>True</c> if the folder is in the user's grouped folders.</returns> /// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
public bool IsFolderGrouped(Guid id) public bool IsFolderGrouped(Guid id)
{ {
return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id); return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1;
} }
private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)

View File

@ -375,14 +375,14 @@ namespace Jellyfin.Server.Implementations.Users
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
AccessSchedules = user.AccessSchedules.ToArray(), AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(), EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(), EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders), EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
SyncPlayAccess = user.SyncPlayAccess, SyncPlayAccess = user.SyncPlayAccess,
BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(), BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(), BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray() BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
} }
}; };
} }
@ -703,13 +703,11 @@ namespace Jellyfin.Server.Implementations.Users
} }
// TODO: fix this at some point // TODO: fix this at some point
user.SetPreference( user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
PreferenceKind.BlockUnratedItems,
policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); 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.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.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user); dbContext.Update(user);

View File

@ -8,7 +8,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -81,7 +80,8 @@ namespace Jellyfin.Server.Migrations.Routines
{ "unstable", ChromecastVersion.Unstable } { "unstable", ChromecastVersion.Unstable }
}; };
var customDisplayPrefs = new HashSet<string>(); var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) 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 itemId = new Guid(result[1].ToBlob());
var dtoUserId = 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); var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null) if (existingUser == null)
{ {
@ -110,7 +119,7 @@ namespace Jellyfin.Server.Migrations.Routines
: ChromecastVersion.Stable; : ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion"); dto.CustomPrefs.Remove("chromecastVersion");
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, result[2].ToString()) var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{ {
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null, IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop, ShowBackdrop = dto.ShowBackdrop,

View File

@ -168,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
} }
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); 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.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.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);

View File

@ -0,0 +1,34 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Legacy DateTime converter.
/// Milliseconds aren't output if zero by default.
/// </summary>
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
/// <inheritdoc />
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetDateTime();
}
/// <inheritdoc />
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);
}
}
}
}

View File

@ -19,56 +19,71 @@ namespace MediaBrowser.Common.Json
/// </summary> /// </summary>
public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\""; public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
/// <summary>
/// When changing these options, update
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
/// -> AddJellyfinApi
/// -> AddJsonOptions.
/// </summary>
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
};
/// <summary> /// <summary>
/// Gets the default <see cref="JsonSerializerOptions" /> options. /// Gets the default <see cref="JsonSerializerOptions" /> options.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When changing these options, update /// The return value must not be modified.
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs /// If the defaults must be modified the author must use the copy constructor.
/// -> AddJellyfinApi
/// -> AddJsonOptions.
/// </remarks> /// </remarks>
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns> /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetOptions() public static JsonSerializerOptions GetOptions()
{ => _jsonSerializerOptions;
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;
}
/// <summary> /// <summary>
/// Gets camelCase json options. /// Gets camelCase json options.
/// </summary> /// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns> /// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetCamelCaseOptions() public static JsonSerializerOptions GetCamelCaseOptions()
{ => _camelCaseJsonSerializerOptions;
var options = GetOptions();
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
return options;
}
/// <summary> /// <summary>
/// Gets PascalCase json options. /// Gets PascalCase json options.
/// </summary> /// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns> /// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetPascalCaseOptions() public static JsonSerializerOptions GetPascalCaseOptions()
{ => _pascalCaseJsonSerializerOptions;
var options = GetOptions();
options.PropertyNamingPolicy = null;
return options;
}
} }
} }

View File

@ -14,6 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup> </ItemGroup>
@ -21,7 +22,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -17,9 +17,10 @@ namespace MediaBrowser.Controller.Channels
{ {
public override bool IsVisible(User user) public override bool IsVisible(User user)
{ {
if (user.GetPreference(PreferenceKind.BlockedChannels) != null) var blockedChannelsPreference = user.GetPreferenceValues<Guid>(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; return false;
} }
@ -27,8 +28,7 @@ namespace MediaBrowser.Controller.Channels
else else
{ {
if (!user.HasPermission(PermissionKind.EnableAllChannels) if (!user.HasPermission(PermissionKind.EnableAllChannels)
&& !user.GetPreference(PreferenceKind.EnabledChannels) && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id))
.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{ {
return false; return false;
} }

View File

@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user) protected override bool GetBlockUnratedValue(User user)
{ {
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString()); return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
} }
public override UnratedItem GetBlockUnratedType() public override UnratedItem GetBlockUnratedType()

View File

@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user) protected override bool GetBlockUnratedValue(User user)
{ {
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString()); return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
} }
public override UnratedItem GetBlockUnratedType() public override UnratedItem GetBlockUnratedType()

View File

@ -480,11 +480,11 @@ namespace MediaBrowser.Controller.Entities
return true; return true;
} }
var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders); var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
if (SourceType == SourceType.Channel) if (SourceType == SourceType.Channel)
{ {
return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase); return allowed.Contains(ChannelId);
} }
else else
{ {
@ -492,7 +492,7 @@ namespace MediaBrowser.Controller.Entities
foreach (var folder in collectionFolders) foreach (var folder in collectionFolders)
{ {
if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) if (allowed.Contains(folder.Id))
{ {
return true; return true;
} }
@ -1909,7 +1909,7 @@ namespace MediaBrowser.Controller.Entities
return false; return false;
} }
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString()); return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType());
} }
/// <summary> /// <summary>

View File

@ -186,13 +186,10 @@ namespace MediaBrowser.Controller.Entities
{ {
if (this is ICollectionFolder && !(this is BasePluginFolder)) if (this is ICollectionFolder && !(this is BasePluginFolder))
{ {
var blockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders); var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
if (blockedMediaFolders.Length > 0) if (blockedMediaFolders.Length > 0)
{ {
if (blockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) || if (blockedMediaFolders.Contains(Id))
// Backwards compatibility
blockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
{ {
return false; return false;
} }
@ -200,8 +197,7 @@ namespace MediaBrowser.Controller.Entities
else else
{ {
if (!user.HasPermission(PermissionKind.EnableAllFolders) if (!user.HasPermission(PermissionKind.EnableAllFolders)
&& !user.GetPreference(PreferenceKind.EnabledFolders) && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{ {
return false; return false;
} }

View File

@ -49,7 +49,7 @@ namespace MediaBrowser.Controller.Entities.Movies
protected override bool GetBlockUnratedValue(User user) protected override bool GetBlockUnratedValue(User user)
{ {
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie.ToString()); return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
} }
public override double GetDefaultPrimaryImageAspectRatio() public override double GetDefaultPrimaryImageAspectRatio()

View File

@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities.TV
protected override bool GetBlockUnratedValue(User user) protected override bool GetBlockUnratedValue(User user)
{ {
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString()); return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
} }
public override UnratedItem GetBlockUnratedType() public override UnratedItem GetBlockUnratedType()

View File

@ -1127,13 +1127,25 @@ namespace MediaBrowser.Controller.MediaEncoding
targetVideoCodec = "hevc"; targetVideoCodec = "hevc";
} }
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault(); var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
profile = Regex.Replace(profile, @"\s+", 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. // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
&& profile != null && profile.Contains("high 10", StringComparison.OrdinalIgnoreCase))
&& profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
{ {
profile = "high"; profile = "high";
} }
@ -1141,8 +1153,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case, // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly). // which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& profile != null && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
{ {
profile = "constrained_baseline"; profile = "constrained_baseline";
} }
@ -1151,16 +1162,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
&& profile != null && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
{ {
profile = "baseline"; 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. // Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
&& profile != null && profile.Contains("main 10", StringComparison.OrdinalIgnoreCase))
&& profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1)
{ {
profile = "main"; profile = "main";
} }

View File

@ -24,7 +24,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.1" /> <PackageReference Include="BDInfo" Version="0.7.6.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.3.0" /> <PackageReference Include="UTF.Unknown" Version="2.3.0" />
</ItemGroup> </ItemGroup>

View File

@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Entities
} }
instance.ProviderIds.TryGetValue(name, out string? id); instance.ProviderIds.TryGetValue(name, out string? id);
return id; return string.IsNullOrEmpty(id) ? null : id;
} }
/// <summary> /// <summary>

View File

@ -33,7 +33,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="5.0.0" /> <PackageReference Include="System.Text.Json" Version="5.0.0" />

View File

@ -22,7 +22,6 @@
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" /> <PackageReference Include="PlaylistsNET" Version="1.1.3" />
<PackageReference Include="TMDbLib" Version="1.7.3-alpha" /> <PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
<PackageReference Include="TvDbSharper" Version="3.2.2" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@ -1,10 +1,16 @@
#pragma warning disable CS1591 #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.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,14 +19,27 @@ namespace MediaBrowser.Providers.TV
{ {
public class SeriesMetadataService : MetadataService<Series, SeriesInfo> public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{ {
private readonly ILocalizationManager _localizationManager;
public SeriesMetadataService( public SeriesMetadataService(
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
ILogger<SeriesMetadataService> logger, ILogger<SeriesMetadataService> logger,
IProviderManager providerManager, IProviderManager providerManager,
IFileSystem fileSystem, IFileSystem fileSystem,
ILibraryManager libraryManager) ILibraryManager libraryManager,
ILocalizationManager localizationManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{ {
_localizationManager = localizationManager;
}
/// <inheritdoc />
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);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -62,5 +81,117 @@ namespace MediaBrowser.Providers.TV
targetItem.AirDays = sourceItem.AirDays; 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<int>();
var virtualSeasons = new List<Season>();
foreach (var existingSeason in series.Children.OfType<Season>())
{
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="series">The series.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
{
var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
.Cast<Episode>()
.Where(i => !i.IsInSeasonFolder);
List<Season> seasons = series.Children.OfType<Season>().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);
}
}
}
/// <summary>
/// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
/// </summary>
/// <param name="series">The series.</param>
/// <param name="seasonNumber">The season number.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The newly created season.</returns>
private async Task<Season> 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;
}
} }
} }

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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

31
deployment/build.linux.arm64 Executable file
View File

@ -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

31
deployment/build.linux.armhf Executable file
View File

@ -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