diff options
65 files changed, 460 insertions, 303 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 87086a728..9c08929e7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -157,6 +157,7 @@ - [jonas-resch](https://github.com/jonas-resch) - [vgambier](https://github.com/vgambier) - [MinecraftPlaye](https://github.com/MinecraftPlaye) + - [RealGreenDragon](https://github.com/RealGreenDragon) # Emby Contributors @@ -225,3 +226,4 @@ - [gnuyent](https://github.com/gnuyent) - [Matthew Jones](https://github.com/matthew-jones-uk) - [Jakob Kukla](https://github.com/jakobkukla) + - [Utku Özdemir](https://github.com/utkuozdemir) diff --git a/Dockerfile b/Dockerfile index c3038b1d2..5ca233b8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,7 +72,7 @@ COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none FROM app diff --git a/Dockerfile.arm b/Dockerfile.arm index b25e6039f..8da17ee5f 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -64,7 +64,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none FROM app diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index d0be834dd..790be1c39 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none FROM app diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 7d82b2cac..e1688dc6e 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -15,7 +15,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="TagLibSharp" Version="2.2.0" /> + <PackageReference Include="TagLibSharp" Version="2.3.0" /> </ItemGroup> <PropertyGroup> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bc55dc6b4..91a16c199 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -83,6 +83,7 @@ using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; +using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; @@ -634,7 +635,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IAuthService, AuthService>(); serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); - serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); + serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>(); + serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 9e653590b..24395a193 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,10 +29,10 @@ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.8" /> <PackageReference Include="Mono.Nat" Version="3.0.3" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" /> - <PackageReference Include="sharpcompress" Version="0.32.1" /> + <PackageReference Include="sharpcompress" Version="0.32.2" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" /> </ItemGroup> diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c54945c93..2843fb8f8 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2454,6 +2454,12 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> + public void QueueLibraryScan() + { + _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>(); + } + + /// <inheritdoc /> public int? GetSeasonNumberFromPath(string path) => SeasonPathParser.Parse(path, true, true).SeasonNumber; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index fe4ccd6ac..a60251dac 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -464,7 +464,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); - if (result.Items.Count == 1) + var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) + || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase); + if (!isPhotosCollection && result.Items.Count == 1) { var videoPath = result.Items[0].Path; var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 3e2dd5be6..9dc2fe799 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -92,22 +92,22 @@ "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", "ValueSpecialEpisodeName": "حلقه خاصه - {0}", "VersionNumber": "النسخة {0}", - "TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.", - "TaskCleanCache": "احذف مجلد ذاكرة التخزين المؤقت", + "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.", + "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة", "TasksChannelsCategory": "قنوات الإنترنت", "TasksLibraryCategory": "مكتبة", "TasksMaintenanceCategory": "صيانة", - "TaskRefreshLibraryDescription": "يقوم بفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة وتحديث البيانات الوصفية.", + "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.", "TaskRefreshLibrary": "افحص مكتبة الوسائط", - "TaskRefreshChapterImagesDescription": "يقوم بانشاء صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.", + "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.", "TaskRefreshChapterImages": "استخراج صور الفصل", "TasksApplicationCategory": "تطبيق", - "TaskDownloadMissingSubtitlesDescription": "يقوم بالبحث في الإنترنت على الترجمات المفقودة إستنادا على البيانات الوصفية.", - "TaskDownloadMissingSubtitles": "تحميل الترجمات المفقودة", - "TaskRefreshChannelsDescription": "يقوم بتحديث معلومات قنوات الإنترنت.", + "TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.", + "TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة", + "TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.", "TaskRefreshChannels": "إعادة تحديث القنوات", - "TaskCleanTranscodeDescription": "يقوم بحذف ملفات الترميز الأقدم من يوم واحد.", - "TaskCleanTranscode": "حذف سجلات الترميز", + "TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.", + "TaskCleanTranscode": "حذف ما بمجلد الترميز", "TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.", "TaskUpdatePlugins": "تحديث الإضافات", "TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.", @@ -116,12 +116,12 @@ "TaskCleanLogs": "حذف مسار السجل", "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.", "TaskCleanActivityLog": "حذف سجل الأنشطة", - "Default": "إفتراضي", + "Default": "افتراضي", "Undefined": "غير معرف", "Forced": "ملحقة", "TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.", "TaskOptimizeDatabase": "تحسين قاعدة البيانات", - "TaskKeyframeExtractorDescription": "يقوم باستخراج الإطارات الرئيسيه من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. هذه المهمه قد تستمر لاوقات طويلة.", + "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "External": "خارجي" } diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 50d019f90..186ec44d2 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -120,5 +120,8 @@ "Forced": "강제하기", "Default": "기본 설정", "TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.", - "TaskOptimizeDatabase": "데이터베이스 최적화" + "TaskOptimizeDatabase": "데이터베이스 최적화", + "TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.", + "TaskKeyframeExtractor": "키프레임 추출", + "External": "외부" } diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index a41523bbd..781e93926 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -120,5 +120,7 @@ "Default": "Подразумевано", "TaskOptimizeDatabase": "Оптимизуј датабазу", "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.", - "External": "Спољно" + "External": "Спољно", + "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.", + "TaskKeyframeExtractor": "Екстрактор кључних сличица" } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 727b9d4b5..d7ab9c021 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -43,9 +43,9 @@ namespace Emby.Server.Implementations.TV } string presentationUniqueKey = null; - if (!string.IsNullOrEmpty(query.SeriesId)) + if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) { - if (_libraryManager.GetItemById(query.SeriesId) is Series series) + if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) { presentationUniqueKey = GetUniqueSeriesKey(series); } @@ -93,9 +93,9 @@ namespace Emby.Server.Implementations.TV string presentationUniqueKey = null; int? limit = null; - if (!string.IsNullOrEmpty(request.SeriesId)) + if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) { - if (_libraryManager.GetItemById(request.SeriesId) is Series series) + if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series) { presentationUniqueKey = GetUniqueSeriesKey(series); limit = 1; @@ -153,7 +153,7 @@ namespace Emby.Server.Implementations.TV // If viewing all next up for all series, remove first episodes // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId); + var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default); var anyFound = false; return allNextUp diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 365e44e1a..1e8d03875 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1790,7 +1790,8 @@ namespace Jellyfin.Api.Controllers || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { if (EncodingHelper.IsCopyCodec(codec) - && (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) + && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) { diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index a61b952f0..4d09070db 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -165,7 +165,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -641,7 +641,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 07e113ad3..138d8b8df 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Description("Gets search hints based on a search term")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<SearchHintResult> Get( + public ActionResult<SearchHintResult> GetSearchHints( [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] Guid? userId, @@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers IndexNumber = item.IndexNumber, ParentIndexNumber = item.ParentIndexNumber, Id = item.Id, - Type = item.GetClientTypeName(), + Type = item.GetBaseItemKind(), MediaType = item.MediaType, MatchedTerm = hintInfo.MatchedTerm, RunTimeTicks = item.RunTimeTicks, @@ -149,8 +149,10 @@ namespace Jellyfin.Api.Controllers EndDate = item.EndDate }; - // legacy +#pragma warning disable CS0618 + // Kept for compatibility with older clients result.ItemId = result.Id; +#pragma warning restore CS0618 if (item.IsFolder) { diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index ea74c7873..cf812fa23 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 179a53fd5..e39d05a6f 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? seriesId, + [FromQuery] Guid? seriesId, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? season, [FromQuery] Guid? seasonId, [FromQuery] bool? isMissing, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] Guid? startItemId, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -278,9 +278,9 @@ namespace Jellyfin.Api.Controllers } // This must be the last filter - if (!string.IsNullOrEmpty(adjacentTo)) + if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default)) { - episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList(); + episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) @@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 309e3a9c5..894d87138 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -17,7 +17,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.7" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.8" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" /> diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index d7c27542f..678f96083 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -27,13 +27,13 @@ <ItemGroup> <PackageReference Include="System.Linq.Async" Version="6.0.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.8" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.8"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index e372742e0..e6bc3fe2b 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -37,8 +37,8 @@ <PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.7" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.7" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.8" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.8" /> <PackageReference Include="prometheus-net" Version="6.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 03d1f3304..bd397bdd1 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio var childUpdateType = ItemUpdateType.None; - // Refresh songs - foreach (var item in items) + // Refresh songs only and not m3u files in album folder + foreach (var item in items.OfType<Audio>()) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index b6983b73e..1860da4c7 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -860,7 +860,7 @@ namespace MediaBrowser.Controller.Entities return true; } - if (!string.IsNullOrEmpty(query.AdjacentTo)) + if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) { Logger.LogDebug("Query requires post-filtering due to AdjacentTo"); return true; @@ -1029,9 +1029,9 @@ namespace MediaBrowser.Controller.Entities #pragma warning restore CA1309 // This must be the last filter - if (!string.IsNullOrEmpty(query.AdjacentTo)) + if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) { - items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo); + items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index db1697c79..13bfd07c3 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Entities public Guid[] ExcludeItemIds { get; set; } - public string? AdjacentTo { get; set; } + public Guid? AdjacentTo { get; set; } public string[] PersonTypes { get; set; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 727efee50..d66802a64 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -266,7 +266,7 @@ namespace MediaBrowser.Controller.Entities.TV DtoOptions = options }; - if (!user.DisplayMissingEpisodes) + if (user == null || !user.DisplayMissingEpisodes) { query.IsMissing = false; } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 2996104e7..f467a6038 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -433,9 +433,9 @@ namespace MediaBrowser.Controller.Entities var user = query.User; // This must be the last filter - if (!string.IsNullOrEmpty(query.AdjacentTo)) + if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) { - items = FilterForAdjacency(items.ToList(), query.AdjacentTo); + items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } return SortAndPage(items, totalRecordLimit, query, libraryManager, true); @@ -985,10 +985,9 @@ namespace MediaBrowser.Controller.Entities return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName); } - public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, string adjacentToId) + public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, Guid adjacentTo) { - var adjacentToIdGuid = new Guid(adjacentToId); - var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentToIdGuid)); + var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentTo)); var index = list.IndexOf(adjacentToItem); @@ -1005,7 +1004,7 @@ namespace MediaBrowser.Controller.Entities nextId = list[index + 1].Id; } - return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentToIdGuid)); + return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentTo)); } } } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 313d27ce6..5905c25a5 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -570,5 +570,13 @@ namespace MediaBrowser.Controller.Library Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason); BaseItem GetParentItem(Guid? parentId, Guid? userId); + + /// <summary> + /// Queue a library scan. + /// </summary> + /// <remarks> + /// This exists so plugins can trigger a library scan. + /// </remarks> + void QueueLibraryScan(); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 15afde601..e5aae620a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -35,6 +35,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; + private readonly Version _minKernelVersioni915Hang = new Version(5, 18); private static readonly string[] _videoProfilesH264 = new[] { @@ -930,6 +931,13 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"'); } + // Disable auto inserted SW scaler for HW decoders in case of changed resolution. + var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options)); + if (!isSwDecoder) + { + arg.Append(" -autoscale 0"); + } + return arg.ToString(); } @@ -1144,16 +1152,15 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.SubtitleStream.IsExternal) { - var subtitlePath = state.SubtitleStream.Path; var charsetParam = string.Empty; if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) { var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet( - subtitlePath, - state.SubtitleStream.Language, - state.MediaSource.Protocol, - CancellationToken.None).GetAwaiter().GetResult(); + state.SubtitleStream, + state.SubtitleStream.Language, + state.MediaSource, + CancellationToken.None).GetAwaiter().GetResult(); if (!string.IsNullOrEmpty(charenc)) { @@ -1165,7 +1172,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, "subtitles=f='{0}'{1}{2}{3}{4}{5}", - _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath), + _mediaEncoder.EscapeSubtitleFilterPath(state.SubtitleStream.Path), charsetParam, alphaParam, sub2videoParam, @@ -1302,6 +1309,10 @@ namespace MediaBrowser.Controller.MediaEncoding // which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping. var intelLowPowerHwEncoding = false; + // Workaround for linux 5.18+ i915 hang at cost of performance. + // https://github.com/intel/media-driver/issues/1456 + var enableWaFori915Hang = false; + if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965; @@ -1317,6 +1328,20 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { + if (OperatingSystem.IsLinux() && Environment.OSVersion.Version >= _minKernelVersioni915Hang) + { + var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty; + var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase) + || vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv") + && IsVaapiSupported(state) + && IsOpenclFullSupported() + && !IsVaapiVppTonemapAvailable(state, encodingOptions) + && IsHwTonemapAvailable(state, encodingOptions); + + enableWaFori915Hang = isIntelDecoder && doOclTonemap; + } + if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerH264HwEncoder; @@ -1325,6 +1350,10 @@ namespace MediaBrowser.Controller.MediaEncoding { intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder; } + else + { + enableWaFori915Hang = false; + } } if (intelLowPowerHwEncoding) @@ -1332,6 +1361,11 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -low_power 1"; } + if (enableWaFori915Hang) + { + param += " -async_depth 1"; + } + var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); @@ -4506,7 +4540,9 @@ namespace MediaBrowser.Controller.MediaEncoding if (isD3d11Supported && isCodecAvailable) { - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); + // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock. + // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11. + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); } } else @@ -5499,7 +5535,7 @@ namespace MediaBrowser.Controller.MediaEncoding return index; } - if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal)) + if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal)) { index++; } diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 4483cf708..5bf83a9e3 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -6,7 +6,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.MediaEncoding { @@ -37,11 +38,11 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the subtitle language encoding parameter. /// </summary> - /// <param name="path">The path.</param> + /// <param name="subtitleStream">The subtitle stream.</param> /// <param name="language">The language.</param> - /// <param name="protocol">The protocol.</param> + /// <param name="mediaSource">The media source.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>System.String.</returns> - Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken); + Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs deleted file mode 100644 index 08ee5c72e..000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.Logging; -using Nikse.SubtitleEdit.Core.SubtitleFormats; - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - /// <summary> - /// Advanced SubStation Alpha subtitle parser. - /// </summary> - public class AssParser : SubtitleEditParser<AdvancedSubStationAlpha> - { - /// <summary> - /// Initializes a new instance of the <see cref="AssParser"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - public AssParser(ILogger logger) : base(logger) - { - } - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs index c0023ebf2..bd13437fb 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System.IO; -using System.Threading; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles @@ -12,8 +11,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// Parses the specified stream. /// </summary> /// <param name="stream">The stream.</param> - /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="fileExtension">The file extension.</param> /// <returns>SubtitleTrackInfo.</returns> - SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken); + SubtitleTrackInfo Parse(Stream stream, string fileExtension); + + /// <summary> + /// Determines whether the file extension is supported by the parser. + /// </summary> + /// <param name="fileExtension">The file extension.</param> + /// <returns>A value indicating whether the file extension is supported.</returns> + bool SupportsFileExtension(string fileExtension); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs deleted file mode 100644 index 78d54ca51..000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.Logging; -using Nikse.SubtitleEdit.Core.SubtitleFormats; - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - /// <summary> - /// SubRip subtitle parser. - /// </summary> - public class SrtParser : SubtitleEditParser<SubRip> - { - /// <summary> - /// Initializes a new instance of the <see cref="SrtParser"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - public SrtParser(ILogger logger) : base(logger) - { - } - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs deleted file mode 100644 index 17c2ae40e..000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.Logging; -using Nikse.SubtitleEdit.Core.SubtitleFormats; - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - /// <summary> - /// SubStation Alpha subtitle parser. - /// </summary> - public class SsaParser : SubtitleEditParser<SubStationAlpha> - { - /// <summary> - /// Initializes a new instance of the <see cref="SsaParser"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - public SsaParser(ILogger logger) : base(logger) - { - } - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index 52c1b6467..eb8ff9624 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -1,12 +1,14 @@ +using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; +using System.Reflection; using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; using Nikse.SubtitleEdit.Core.Common; -using ILogger = Microsoft.Extensions.Logging.ILogger; +using Nikse.SubtitleEdit.Core.SubtitleFormats; using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat; namespace MediaBrowser.MediaEncoding.Subtitles @@ -14,31 +16,57 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// SubStation Alpha subtitle parser. /// </summary> - /// <typeparam name="T">The <see cref="SubtitleFormat" />.</typeparam> - public abstract class SubtitleEditParser<T> : ISubtitleParser - where T : SubtitleFormat, new() + public class SubtitleEditParser : ISubtitleParser { - private readonly ILogger _logger; + private readonly ILogger<SubtitleEditParser> _logger; + private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats; /// <summary> - /// Initializes a new instance of the <see cref="SubtitleEditParser{T}"/> class. + /// Initializes a new instance of the <see cref="SubtitleEditParser"/> class. /// </summary> /// <param name="logger">The logger.</param> - protected SubtitleEditParser(ILogger logger) + public SubtitleEditParser(ILogger<SubtitleEditParser> logger) { _logger = logger; + _subtitleFormats = GetSubtitleFormats() + .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension)) + .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase); } /// <inheritdoc /> - public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) + public SubtitleTrackInfo Parse(Stream stream, string fileExtension) { var subtitle = new Subtitle(); - var subRip = new T(); var lines = stream.ReadAllLines().ToList(); - subRip.LoadSubtitle(subtitle, lines, "untitled"); - if (subRip.ErrorCount > 0) + + if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats)) + { + throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension)); + } + + foreach (var subtitleFormat in subtitleFormats) { - _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount); + _logger.LogDebug( + "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", + fileExtension, + subtitleFormat.Name); + subtitleFormat.LoadSubtitle(subtitle, lines, fileExtension); + if (subtitleFormat.ErrorCount == 0) + { + break; + } + + _logger.LogError( + "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", + subtitleFormat.ErrorCount, + fileExtension, + subtitleFormat.Name); + } + + if (subtitle.Paragraphs.Count == 0) + { + throw new ArgumentException("Unsupported format: " + fileExtension); } var trackInfo = new SubtitleTrackInfo(); @@ -57,5 +85,36 @@ namespace MediaBrowser.MediaEncoding.Subtitles trackInfo.TrackEvents = trackEvents; return trackInfo; } + + /// <inheritdoc /> + public bool SupportsFileExtension(string fileExtension) + => _subtitleFormats.ContainsKey(fileExtension); + + private IEnumerable<SubtitleFormat> GetSubtitleFormats() + { + var subtitleFormats = new List<SubtitleFormat>(); + var assembly = typeof(SubtitleFormat).Assembly; + + foreach (var type in assembly.GetTypes()) + { + if (!type.IsSubclassOf(typeof(SubtitleFormat)) || type.IsAbstract) + { + continue; + } + + try + { + // It shouldn't be null, but the exception is caught if it is + var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!; + subtitleFormats.Add(subtitleFormat); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to create instance of the subtitle format {SubtitleFormatType}", type.Name); + } + } + + return subtitleFormats; + } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 7091af734..8e1acc46c 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -35,6 +35,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IMediaEncoder _mediaEncoder; private readonly IHttpClientFactory _httpClientFactory; private readonly IMediaSourceManager _mediaSourceManager; + private readonly ISubtitleParser _subtitleParser; /// <summary> /// The _semaphoreLocks. @@ -48,7 +49,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles IFileSystem fileSystem, IMediaEncoder mediaEncoder, IHttpClientFactory httpClientFactory, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + ISubtitleParser subtitleParser) { _logger = logger; _appPaths = appPaths; @@ -56,6 +58,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _mediaEncoder = mediaEncoder; _httpClientFactory = httpClientFactory; _mediaSourceManager = mediaSourceManager; + _subtitleParser = subtitleParser; } private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); @@ -73,8 +76,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - var reader = GetReader(inputFormat); - var trackInfo = reader.Parse(stream, cancellationToken); + var trackInfo = _subtitleParser.Parse(stream, inputFormat); FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); @@ -233,54 +235,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) .TrimStart('.'); - if (!TryGetReader(currentFormat, out _)) + // Fallback to ffmpeg conversion + if (!_subtitleParser.SupportsFileExtension(currentFormat)) { // Convert var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); - await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); + await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); } - // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) + // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true); } - private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value) - { - if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) - { - value = new SrtParser(_logger); - return true; - } - - if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)) - { - value = new SsaParser(_logger); - return true; - } - - if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) - { - value = new AssParser(_logger); - return true; - } - - value = null; - return false; - } - - private ISubtitleParser GetReader(string format) - { - if (TryGetReader(format, out var reader)) - { - return reader; - } - - throw new ArgumentException("Unsupported format: " + format); - } - private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) { if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) @@ -351,13 +320,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// Converts the text subtitle to SRT. /// </summary> - /// <param name="inputPath">The input path.</param> - /// <param name="language">The language.</param> + /// <param name="subtitleStream">The subtitle stream.</param> /// <param name="mediaSource">The input mediaSource.</param> /// <param name="outputPath">The output path.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) + private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) { var semaphore = GetLock(outputPath); @@ -367,7 +335,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); + await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } } finally @@ -379,8 +347,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// Converts the text subtitle to SRT internal. /// </summary> - /// <param name="inputPath">The input path.</param> - /// <param name="language">The language.</param> + /// <param name="subtitleStream">The subtitle stream.</param> /// <param name="mediaSource">The input mediaSource.</param> /// <param name="outputPath">The output path.</param> /// <param name="cancellationToken">The cancellation token.</param> @@ -388,8 +355,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <exception cref="ArgumentNullException"> /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>. /// </exception> - private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) + private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) { + var inputPath = subtitleStream.Path; if (string.IsNullOrEmpty(inputPath)) { throw new ArgumentNullException(nameof(inputPath)); @@ -402,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath))); - var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); + var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, cancellationToken).ConfigureAwait(false); // FFmpeg automatically convert character encoding when it is UTF-16 // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" @@ -420,18 +388,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles int exitCode; using (var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _mediaEncoder.EncoderPath, - Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -571,7 +539,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var processArgs = string.Format( CultureInfo.InvariantCulture, - "-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", + "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, subtitleStreamIndex, outputCodec, @@ -580,18 +548,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles int exitCode; using (var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _mediaEncoder.EncoderPath, - Arguments = processArgs, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = processArgs, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -606,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw; } - var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false); + var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); if (!ranToCompletion) { @@ -729,9 +697,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles } /// <inheritdoc /> - public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken) + public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken) { - using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + var subtitleCodec = subtitleStream.Codec; + var path = subtitleStream.Path; + + if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + { + path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec); + await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken) + .ConfigureAwait(false); + } + + using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false)) { var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty; @@ -754,12 +732,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles switch (protocol) { case MediaProtocol.Http: - { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(new Uri(path), cancellationToken) - .ConfigureAwait(false); - return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - } + { + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(new Uri(path), cancellationToken) + .ConfigureAwait(false); + return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } case MediaProtocol.File: return AsyncFile.OpenRead(path); diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index ce2170793..ae8f3b0ed 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -594,6 +594,10 @@ namespace MediaBrowser.Model.Entities <= 426 when Height <= 240 => IsInterlaced ? "240i" : "240p", // 640x360 (16:9 square pixel format) <= 640 when Height <= 360 => IsInterlaced ? "360i" : "360p", + // 682x384 (16:9 square pixel format) + <= 682 when Height <= 384 => IsInterlaced ? "384i" : "384p", + // 720x404 (16:9 square pixel format) + <= 720 when Height <= 404 => IsInterlaced ? "404i" : "404p", // 854x480 (16:9 square pixel format) <= 854 when Height <= 480 => IsInterlaced ? "480i" : "480p", // 960x544 (16:9 square pixel format) diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 133d6a916..0fb996df9 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Model.Querying /// Gets or sets the series id. /// </summary> /// <value>The series id.</value> - public string SeriesId { get; set; } + public Guid? SeriesId { get; set; } /// <summary> /// Gets or sets the start index. Use for paging. diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 983dbd2bc..4696c3797 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -1,8 +1,6 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Search { @@ -12,11 +10,27 @@ namespace MediaBrowser.Model.Search public class SearchHint { /// <summary> + /// Initializes a new instance of the <see cref="SearchHint" /> class. + /// </summary> + public SearchHint() + { + Name = string.Empty; + MatchedTerm = string.Empty; + MediaType = string.Empty; + Artists = Array.Empty<string>(); + } + + /// <summary> /// Gets or sets the item id. /// </summary> /// <value>The item id.</value> + [Obsolete("Use Id instead")] public Guid ItemId { get; set; } + /// <summary> + /// Gets or sets the item id. + /// </summary> + /// <value>The item id.</value> public Guid Id { get; set; } /// <summary> @@ -53,38 +67,42 @@ namespace MediaBrowser.Model.Search /// Gets or sets the image tag. /// </summary> /// <value>The image tag.</value> - public string PrimaryImageTag { get; set; } + public string? PrimaryImageTag { get; set; } /// <summary> /// Gets or sets the thumb image tag. /// </summary> /// <value>The thumb image tag.</value> - public string ThumbImageTag { get; set; } + public string? ThumbImageTag { get; set; } /// <summary> /// Gets or sets the thumb image item identifier. /// </summary> /// <value>The thumb image item identifier.</value> - public string ThumbImageItemId { get; set; } + public string? ThumbImageItemId { get; set; } /// <summary> /// Gets or sets the backdrop image tag. /// </summary> /// <value>The backdrop image tag.</value> - public string BackdropImageTag { get; set; } + public string? BackdropImageTag { get; set; } /// <summary> /// Gets or sets the backdrop image item identifier. /// </summary> /// <value>The backdrop image item identifier.</value> - public string BackdropImageItemId { get; set; } + public string? BackdropImageItemId { get; set; } /// <summary> /// Gets or sets the type. /// </summary> /// <value>The type.</value> - public string Type { get; set; } + public BaseItemKind Type { get; set; } + /// <summary> + /// Gets a value indicating whether this instance is folder. + /// </summary> + /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> public bool? IsFolder { get; set; } /// <summary> @@ -99,31 +117,47 @@ namespace MediaBrowser.Model.Search /// <value>The type of the media.</value> public string MediaType { get; set; } + /// <summary> + /// Gets or sets the start date. + /// </summary> + /// <value>The start date.</value> public DateTime? StartDate { get; set; } + /// <summary> + /// Gets or sets the end date. + /// </summary> + /// <value>The end date.</value> public DateTime? EndDate { get; set; } /// <summary> /// Gets or sets the series. /// </summary> /// <value>The series.</value> - public string Series { get; set; } + public string? Series { get; set; } - public string Status { get; set; } + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public string? Status { get; set; } /// <summary> /// Gets or sets the album. /// </summary> /// <value>The album.</value> - public string Album { get; set; } + public string? Album { get; set; } - public Guid AlbumId { get; set; } + /// <summary> + /// Gets or sets the album id. + /// </summary> + /// <value>The album id.</value> + public Guid? AlbumId { get; set; } /// <summary> /// Gets or sets the album artist. /// </summary> /// <value>The album artist.</value> - public string AlbumArtist { get; set; } + public string? AlbumArtist { get; set; } /// <summary> /// Gets or sets the artists. @@ -147,13 +181,13 @@ namespace MediaBrowser.Model.Search /// Gets or sets the channel identifier. /// </summary> /// <value>The channel identifier.</value> - public Guid ChannelId { get; set; } + public Guid? ChannelId { get; set; } /// <summary> /// Gets or sets the name of the channel. /// </summary> /// <value>The name of the channel.</value> - public string ChannelName { get; set; } + public string? ChannelName { get; set; } /// <summary> /// Gets or sets the primary image aspect ratio. diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index f49492f33..c09b6d813 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -40,6 +41,7 @@ namespace MediaBrowser.Providers.TV { await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); + RemoveObsoleteEpisodes(item); RemoveObsoleteSeasons(item); await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); } @@ -121,6 +123,61 @@ namespace MediaBrowser.Providers.TV } } + private void RemoveObsoleteEpisodes(Series series) + { + var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList(); + var numberOfEpisodes = episodes.Count; + // TODO: O(n^2), but can it be done faster without overcomplicating it? + for (var i = 0; i < numberOfEpisodes; i++) + { + var currentEpisode = episodes[i]; + // The outer loop only examines virtual episodes + if (!currentEpisode.IsVirtualItem) + { + continue; + } + + // Virtual episodes without an episode number are practically orphaned and should be deleted + if (!currentEpisode.IndexNumber.HasValue) + { + DeleteEpisode(currentEpisode); + continue; + } + + for (var j = i + 1; j < numberOfEpisodes; j++) + { + var comparisonEpisode = episodes[j]; + // The inner loop is only for "physical" episodes + if (comparisonEpisode.IsVirtualItem + || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber + || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value)) + { + continue; + } + + DeleteEpisode(currentEpisode); + break; + } + } + } + + private void DeleteEpisode(Episode episode) + { + Logger.LogInformation( + "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}", + episode.ParentIndexNumber, + episode.IndexNumber, + episode.SeriesName); + + LibraryManager.DeleteItem( + episode, + 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. diff --git a/debian/rules b/debian/rules index 64e2b48ea..f55b1807e 100755 --- a/debian/rules +++ b/debian/rules @@ -40,7 +40,7 @@ override_dh_clistrip: override_dh_auto_build: dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ - "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server + -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server override_dh_auto_clean: dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 89c74aadb..81d75c1aa 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-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 diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64 index b2bd40713..3fd3fa33c 100644 --- a/deployment/Dockerfile.docker.amd64 +++ b/deployment/Dockerfile.docker.amd64 @@ -10,4 +10,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64 index fc60f1624..e3cc92bcb 100644 --- a/deployment/Dockerfile.docker.arm64 +++ b/deployment/Dockerfile.docker.arm64 @@ -10,4 +10,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf index f5cc47d83..3a5df2e24 100644 --- a/deployment/Dockerfile.docker.armhf +++ b/deployment/Dockerfile.docker.armhf @@ -10,4 +10,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # because of changes in docker and systemd we need to not build in parallel at the moment # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none" +RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 2135d6f01..4139ed96d 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-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 diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 24330f629..313a3e5cb 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-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 diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 507f446cc..693ee7c27 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-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 diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 31513541c..e7765a5b2 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-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 diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64 index a1e7f661a..05059e4ed 100755 --- a/deployment/build.linux.amd64 +++ b/deployment/build.linux.amd64 @@ -16,7 +16,7 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.linux.amd64-musl b/deployment/build.linux.amd64-musl index 72444c05e..0ee4b05fb 100755 --- a/deployment/build.linux.amd64-musl +++ b/deployment/build.linux.amd64-musl @@ -16,7 +16,7 @@ else 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" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true tar -czf jellyfin-server_${version}_linux-amd64-musl.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64 index e362607a7..6e36db0eb 100755 --- a/deployment/build.linux.arm64 +++ b/deployment/build.linux.arm64 @@ -16,7 +16,7 @@ else 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" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true tar -czf jellyfin-server_${version}_linux-arm64.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf index c0d0607fc..f83eeebf1 100755 --- a/deployment/build.linux.armhf +++ b/deployment/build.linux.armhf @@ -16,7 +16,7 @@ else 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" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true tar -czf jellyfin-server_${version}_linux-armhf.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.macos b/deployment/build.macos index 6255c80cb..01c640c8b 100755 --- a/deployment/build.macos +++ b/deployment/build.macos @@ -16,7 +16,7 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.portable b/deployment/build.portable index a6c741881..27e5e987f 100755 --- a/deployment/build.portable +++ b/deployment/build.portable @@ -16,7 +16,7 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=false" +dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=false tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version} rm -rf dist/jellyfin-server_${version} diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index a9daa6a23..30b252beb 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -23,7 +23,7 @@ fi output_dir="dist/jellyfin-server_${version}" # Build binary -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true" +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true # Prepare addins addin_build_dir="$( mktemp -d )" diff --git a/fedora/jellyfin.override.conf b/fedora/jellyfin.override.conf index 8652450bb..48b4de1e9 100644 --- a/fedora/jellyfin.override.conf +++ b/fedora/jellyfin.override.conf @@ -5,3 +5,49 @@ [Service] #User = jellyfin #EnvironmentFile = /etc/sysconfig/jellyfin + +# Service hardening options +# These were added in PR #6953 to solve issue #6952, but some combination of +# them causes "restart.sh" functionality to break with the following error: +# sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the +# 'nosuid' option set or an NFS file system without root privileges? +# See issue #7503 for details on the troubleshooting that went into this. +# Since these were added for NixOS specifically and are above and beyond +# what 99% of systemd units do, they have been moved here as optional +# additional flags to set for maximum system security and can be enabled at +# the administrator's or package maintainer's discretion. +# Uncomment these only if you know what you're doing, and doing so may cause +# bugs with in-server Restart and potentially other functionality as well. +#NoNewPrivileges=true +#SystemCallArchitectures=native +#RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +#RestrictNamespaces=false +#RestrictRealtime=true +#RestrictSUIDSGID=true +#ProtectClock=true +#ProtectControlGroups=false +#ProtectHostname=true +#ProtectKernelLogs=false +#ProtectKernelModules=false +#ProtectKernelTunables=false +#LockPersonality=true +#PrivateTmp=false +#PrivateDevices=false +#PrivateUsers=true +#RemoveIPC=true +#SystemCallFilter=~@clock +#SystemCallFilter=~@aio +#SystemCallFilter=~@chown +#SystemCallFilter=~@cpu-emulation +#SystemCallFilter=~@debug +#SystemCallFilter=~@keyring +#SystemCallFilter=~@memlock +#SystemCallFilter=~@module +#SystemCallFilter=~@mount +#SystemCallFilter=~@obsolete +#SystemCallFilter=~@privileged +#SystemCallFilter=~@raw-io +#SystemCallFilter=~@reboot +#SystemCallFilter=~@setuid +#SystemCallFilter=~@swap +#SystemCallErrorNumber=EPERM diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service index 1193ddb5b..eb0d64087 100644 --- a/fedora/jellyfin.service +++ b/fedora/jellyfin.service @@ -13,39 +13,5 @@ Restart = on-failure TimeoutSec = 15 SuccessExitStatus=0 143 -NoNewPrivileges=true -SystemCallArchitectures=native -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK -RestrictNamespaces=false -RestrictRealtime=true -RestrictSUIDSGID=true -ProtectClock=true -ProtectControlGroups=false -ProtectHostname=true -ProtectKernelLogs=false -ProtectKernelModules=false -ProtectKernelTunables=false -LockPersonality=true -PrivateTmp=false -PrivateDevices=false -PrivateUsers=true -RemoveIPC=true -SystemCallFilter=~@clock -SystemCallFilter=~@aio -SystemCallFilter=~@chown -SystemCallFilter=~@cpu-emulation -SystemCallFilter=~@debug -SystemCallFilter=~@keyring -SystemCallFilter=~@memlock -SystemCallFilter=~@module -SystemCallFilter=~@mount -SystemCallFilter=~@obsolete -SystemCallFilter=~@privileged -SystemCallFilter=~@raw-io -SystemCallFilter=~@reboot -SystemCallFilter=~@setuid -SystemCallFilter=~@swap -SystemCallErrorNumber=EPERM - [Install] WantedBy = multi-user.target diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 13f21ea1f..a6771e389 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -68,7 +68,7 @@ export DOTNET_CLI_TELEMETRY_OPTOUT=1 export PATH=$PATH:/usr/local/bin # cannot use --output due to https://github.com/dotnet/sdk/issues/22220 dotnet publish --configuration Release --self-contained --runtime %{dotnet_runtime} \ - "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server + -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server %install diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 1f1e2910a..3e610ced9 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -15,7 +15,7 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" /> <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageReference Include="xunit" Version="2.4.1" /> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs index 3775555de..e14850eed 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs @@ -15,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { using (var stream = File.OpenRead("Test Data/example.ass")) { - var parsed = new AssParser(new NullLogger<AssParser>()).Parse(stream, CancellationToken.None); + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass"); Assert.Single(parsed.TrackEvents); var trackEvent = parsed.TrackEvents[0]; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs index c07c9ea7d..0038b1873 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs @@ -15,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { using (var stream = File.OpenRead("Test Data/example.srt")) { - var parsed = new SrtParser(new NullLogger<SrtParser>()).Parse(stream, CancellationToken.None); + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); Assert.Equal(2, parsed.TrackEvents.Count); var trackEvent1 = parsed.TrackEvents[0]; @@ -37,7 +37,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { using (var stream = File.OpenRead("Test Data/example2.srt")) { - var parsed = new SrtParser(new NullLogger<SrtParser>()).Parse(stream, CancellationToken.None); + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); Assert.Equal(2, parsed.TrackEvents.Count); var trackEvent1 = parsed.TrackEvents[0]; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs index 56649db8f..3b9a71690 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs @@ -13,7 +13,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { public class SsaParserTests { - private readonly SsaParser _parser = new SsaParser(new NullLogger<AssParser>()); + private readonly SubtitleEditParser _parser = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()); [Theory] [MemberData(nameof(Parse_MultipleDialogues_TestData))] @@ -21,7 +21,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa))) { - SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, CancellationToken.None); + SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa"); Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count); @@ -76,7 +76,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { using (var stream = File.OpenRead("Test Data/example.ssa")) { - var parsed = _parser.Parse(stream, CancellationToken.None); + var parsed = _parser.Parse(stream, "ssa"); Assert.Single(parsed.TrackEvents); var trackEvent = parsed.TrackEvents[0]; diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs index 7c8a90605..80c38affe 100644 --- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs @@ -127,14 +127,14 @@ namespace Jellyfin.Model.Tests.Entities [InlineData(2560, 1080, true, "1080i")] [InlineData(4096, 3072, false, "4K")] [InlineData(8192, 6144, false, "8K")] - [InlineData(512, 384, false, "480p")] + [InlineData(512, 384, false, "384p")] [InlineData(576, 336, false, "360p")] [InlineData(576, 336, true, "360i")] [InlineData(624, 352, false, "360p")] [InlineData(640, 352, false, "360p")] [InlineData(640, 480, false, "480p")] - [InlineData(704, 396, false, "480p")] - [InlineData(720, 404, false, "480p")] + [InlineData(704, 396, false, "404p")] + [InlineData(720, 404, false, "404p")] [InlineData(720, 480, false, "480p")] [InlineData(720, 576, false, "576p")] [InlineData(768, 576, false, "576p")] diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index e2a0fe89b..9d6776b07 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -9,7 +9,7 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" /> <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageReference Include="xunit" Version="2.4.1" /> diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index fb90c69de..f19e33061 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -10,7 +10,7 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.8" /> <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageReference Include="xunit" Version="2.4.1" /> |
