diff options
47 files changed, 488 insertions, 318 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index c2acaaccb..31ae50263 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -1,59 +1,131 @@ name: Issue Report description: File an issue report -title: "[Issue]: " labels: [bug, triage] body: - type: markdown + id: introduction attributes: value: | - Thanks for taking the time to report an issue. Before submitting a report, please do the following: - 1. Please head to our forum or chat rooms and troubleshoot with volunteers if you haven't already. Links can be found here: https://jellyfin.org/contact/ - 2. Please search the bug tracker for similar issues. If you do find one, please comment there instead of opening a new bug report. - 3. If you decide to open a new report, please provide as much detail as possible. - 4. Please **ONLY** report **ONE** issue per report. If you are experiencing multiple issues, please open multiple reports. + ### Thank you for taking the time to report an issue! + Please keep in mind that Jellyfin is a [free and open-source](https://jellyfin.org/docs/general/about) project, made up entirely and exclusively of **volunteers** who donate their free time to the project. + - type: checkboxes + id: before-posting + attributes: + label: "This issue respects the following points:" + description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment. + options: + - label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/). + required: true + - label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_. + required: true + - label: I'm using an up to date version of Jellyfin Server stable, unstable or master; We generally do not support previous older versions. If possible, please update to the latest version before opening an issue. + required: true + - label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct). + required: true + - label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one. + required: true + - type: markdown + id: preliminary-information + attributes: + value: | + ### General preliminary information + + Please keep the following in mind when creating this issue: + + 1. Fill in as much of the template as possible. When you are unsure about the relevancy of a section, do include the information requested in that section. Only leave out information in sections when you are completely sure about it not being relevant. + 2. Provide as much detail as possible. Do not assume other people to know what is going on. + 3. Keep everything readable and structured. Nobody enjoys reading poorly written reports that are difficult to understand. + 4. Keep an eye on your report as long as it is open, your involvement might be requested at a later moment. + 5. Keep the title short and descriptive. The title is not the place to write down a full description of the issue. + 6. When deciding to leave out information in a field, leave it blank and empty. Avoid writing things such as `n/a` for empty fields. - type: textarea - id: what-happened + id: bug-description attributes: - label: Please describe your bug - description: Also tell us, what did you expect to happen? + label: Description of the bug + description: Please provide a detailed description on the bug you encountered, in a readable and comprehensible way. placeholder: | - The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful. - If you are using an old release of Jellyfin, please also explain why. + After upgrading to version x.y.z of Jellyfin, the "login disclaimer" is showing incorrect text. It appears to me that it is appending the server name to the end of the login disclaimer, and showing that to a user. It might be a regression from pull request x. I have tried rebooting my host as well as my container multiple times. I tested this functionality on different clients, and it happens to all the tested clients (client x, y, z), that support the login disclaimer functionality. This makes me believe it is a server side issue. validations: required: true - type: textarea id: repro-steps attributes: - label: Reproduction Steps + label: Reproduction steps + description: Reproduction steps should be complete and self-contained. Anyone can reproduce this issue by following these steps. Furthermore, the steps should be clear and easy to follow. + placeholder: | + 1. Sign in on the Jellyfin web client, with an admin account, using a browser of your choice. + 2. Navigate to the dashboard. + 3. Select "general". + 4. Change the login disclaimer to something like "I am a cool disclaimer!" + 5. Save the settings. + 6. Sign out. + 7. Make sure you are on the sign in screen. Otherwise, navigate to the sign in screen manually. + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: What is the current _bug_ behavior? + description: Write down the incorrect behavior that currently happens after following the reproduction steps. + placeholder: | + The login disclaimer on the sign in screen has the server name appended to the text. The text shown is: "I am a cool disclaimer!jellyfinserver". + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: What is the expected _correct_ behavior? + description: Write down the correct expected behavior that is supposed to happen after following the reproduction steps. placeholder: | - 1. In this environment... - 2. With this config... - 3. Run '...' - 4. See error... + The login disclaimer on the sign in screen should only show the configured text. The text that should be shown is: "I am a cool disclaimer!". validations: required: true - type: dropdown id: version attributes: - label: Jellyfin Version - description: What version of Jellyfin are you running? + label: Jellyfin Server version + description: What version of Jellyfin are you using? options: - - 10.9.0 - - 10.8.13 - - 10.8.12 or older (please specify) - - Weekly unstable (please specify) - - Master branch + - 10.9.7 + - Master + - Unstable + - Older* validations: required: true - type: input - id: version-other + id: version-master + attributes: + label: "Specify commit id" + description: Fill in this field in case the option 'master' is selected. Provide the commit id it was built on. + placeholder: | + 610e56baafc3011e1bfa043bdabb567bda0c2ab0 + - type: input + id: version-unstable + attributes: + label: "Specify unstable release number" + description: Fill in this field in case the option 'unstable' is selected. Provide the unstable release number. + placeholder: | + 2024050906 + - type: input + id: version-older attributes: - label: "if other:" - placeholder: Other + label: "Specify version number" + description: Fill in this field in case the option 'older' is selected. Provide the version number. + placeholder: | + x.y.z + - type: input + id: build-version + attributes: + label: "Specify the build version" + description: Please provide the build version that is shown in the dashboard. + validations: + required: true - type: textarea + id: environment-information attributes: label: Environment description: | + Accurately fill in as much environment details as possible. If a certain environment field is not shown in the template below, but you consider useful information, please include it. Examples: - **OS**: [e.g. Debian 11, Windows 10] - **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.] @@ -88,21 +160,22 @@ body: validations: required: true - type: markdown + id: general-information-logs attributes: value: | - When providing logs, please keep the following things in mind. - 1. **DO NOT** use external paste services. + When providing logs, please keep the following things in mind: + 1. **DO NOT** use external paste services. If logs are too large to paste into the field, upload them as text files. 2. Please provide complete logs. - - For server logs, include everything you think is important plus *10 lines before and after* + - For server logs, ensure to capture all relevant information, encompassing both the events leading up to and following the occurrence of the issue. Typically, providing 10 *lines preceding and succeeding* the problem should be adequate. - For ffmpeg logs, please provide the entire file unmodified. - 3. Please do not run logs through any translation program. Especially beware if your browser translates pages by default. + 3. Please do not run logs through any translation program. We exclusively accept raw, untranslated logs. Particularly exercise caution if your browser automatically translates pages by default. + - Do not forget to censor out personal information such as public IP addresses. 4. Please do not include logs as screenshots, with the only exception being client logs in browsers. - type: textarea - id: logs + id: jellyfin-logs attributes: label: Jellyfin logs description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs. - placeholder: For playback issues, browser/client and FFmpeg logs may be more useful. render: shell validations: required: true @@ -110,24 +183,20 @@ body: id: ffmpeg-logs attributes: label: FFmpeg logs - description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log. - placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg. + description: Relevant FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log. This field is considered mandatory for transcoding related issues. It's also important to include the specific codec details. render: shell - type: textarea - id: browserlogs + id: browser-logs attributes: - label: Please attach any browser or client logs here - placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation. + label: Client / Browser logs + description: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation. - type: textarea id: screenshots attributes: - label: Please attach any screenshots here - placeholder: Images can be pasted directly into the textbox and will be hosted by github. - - type: checkboxes - id: terms + label: Relevant screenshots or videos + description: Attach relevant screenshots or videos related to this report. + - type: textarea + id: additional-information attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct) - options: - - label: I agree to follow this project's Code of Conduct - required: true + label: Additional information + description: Any additional information that might be useful to this issue. diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index e9ae80b93..c6ea1d7ca 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 0ac955cc4..91c2be87b 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -34,7 +34,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@345a7c3866622fe946263314218ad2c12c388b8b # 5.3.7 + uses: danielpalme/ReportGenerator-GitHub-Action@5808021ec4deecb0ab3da051d49b4ce65fcc20af # 5.3.8 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8f63bd6b4..edbc846d6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -185,6 +185,7 @@ - [Vedant](https://github.com/viktory36/) - [NotSaifA](https://github.com/NotSaifA) - [HonestlyWhoKnows](https://github.com/honestlywhoknows) + - [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode) # Emby Contributors diff --git a/Directory.Packages.props b/Directory.Packages.props index f2127c8a6..825301bfc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,8 +84,8 @@ <PackageVersion Include="TMDbLib" Version="2.2.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.8.1" /> + <PackageVersion Include="xunit" Version="2.9.0" /> </ItemGroup> </Project>
\ No newline at end of file diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 39524be1d..dc845b2d7 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -104,6 +104,6 @@ namespace Emby.Server.Implementations.AppBase /// Gets the folder path to the temp directory within the cache folder. /// </summary> /// <value>The temp directory.</value> - public string TempDirectory => Path.Combine(CachePath, "temp"); + public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c394b25bd..5bf9c4fc2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -664,7 +664,8 @@ namespace Emby.Server.Implementations GetExports<IMetadataService>(), GetExports<IMetadataProvider>(), GetExports<IMetadataSaver>(), - GetExports<IExternalId>()); + GetExports<IExternalId>(), + GetExports<IExternalUrlProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 81ee55d26..c2e3312ab 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1046,9 +1046,10 @@ namespace Emby.Server.Implementations.Data foreach (var part in value.SpanSplit('|')) { var providerDelimiterIndex = part.IndexOf('='); - if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('=')) + // Don't let empty values through + if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) { - item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString()); + item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 250bec9ea..28bb29df8 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -466,7 +466,7 @@ namespace Emby.Server.Implementations.IO File.Copy(file1, temp1, true); File.Copy(file2, file1, true); - File.Copy(temp1, file2, true); + File.Move(temp1, file2, true); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 0a3d740cc..82db7c46b 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -122,6 +122,7 @@ namespace Emby.Server.Implementations.Images } await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false); + File.Delete(outputPath); return ItemUpdateType.ImageUpdate; } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ac2248264..cbded1ec6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#pragma warning disable CA5394 using System; using System.Collections.Concurrent; @@ -16,6 +17,7 @@ using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks.Tasks; +using Emby.Server.Implementations.Sorting; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -1710,13 +1712,19 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder) { - var isFirst = true; - IOrderedEnumerable<BaseItem>? orderedItems = null; foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null)) { - if (isFirst) + if (orderBy is RandomComparer) + { + var randomItems = items.ToArray(); + Random.Shared.Shuffle(randomItems); + items = randomItems; + // Items are no longer ordered at this point, so set orderedItems back to null + orderedItems = null; + } + else if (orderedItems is null) { orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) @@ -1728,8 +1736,6 @@ namespace Emby.Server.Implementations.Library ? orderedItems!.ThenByDescending(i => i, orderBy) : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration } - - isFirst = false; } return orderedItems ?? items; @@ -1738,8 +1744,6 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy) { - var isFirst = true; - IOrderedEnumerable<BaseItem>? orderedItems = null; foreach (var (name, sortOrder) in orderBy) @@ -1750,7 +1754,15 @@ namespace Emby.Server.Implementations.Library continue; } - if (isFirst) + if (comparer is RandomComparer) + { + var randomItems = items.ToArray(); + Random.Shared.Shuffle(randomItems); + items = randomItems; + // Items are no longer ordered at this point, so set orderedItems back to null + orderedItems = null; + } + else if (orderedItems is null) { orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) @@ -1762,8 +1774,6 @@ namespace Emby.Server.Implementations.Library ? orderedItems!.ThenByDescending(i => i, comparer) : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration } - - isFirst = false; } return orderedItems ?? items; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 5ec333cb1..bb22ca82f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -113,6 +113,11 @@ namespace Emby.Server.Implementations.Library return true; } + if (stream.IsPgsSubtitleStream) + { + return true; + } + return false; } diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 845dce5df..9172af516 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -52,7 +52,7 @@ "UserDownloadingItemWithValues": "{0} спампоўваецца {1}", "TaskOptimizeDatabase": "Аптымізаваць базу дадзеных", "Artists": "Выканаўцы", - "UserOfflineFromDevice": "{0} адключыўся ад {1}", + "UserOfflineFromDevice": "{0} адлучыўся ад {1}", "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", "TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.", "TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.", @@ -66,7 +66,7 @@ "AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}", "Books": "Кнігі", "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}", - "DeviceOfflineWithName": "{0} адключыўся", + "DeviceOfflineWithName": "{0} адлучыўся", "DeviceOnlineWithName": "{0} падлучаны", "Forced": "Прымусова", "HeaderRecordingGroups": "Групы запісаў", diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json index 1f982feaf..a25099ee0 100644 --- a/Emby.Server.Implementations/Localization/Core/pa.json +++ b/Emby.Server.Implementations/Localization/Core/pa.json @@ -104,7 +104,7 @@ "Forced": "ਮਜਬੂਰ", "Folders": "ਫੋਲਡਰ", "Favorites": "ਮਨਪਸੰਦ", - "FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ", + "FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ", "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ", "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ", "Default": "ਡਿਫੌਲਟ", @@ -119,5 +119,6 @@ "AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}", "Albums": "ਐਲਬਮਾਂ", "TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ", - "External": "ਬਾਹਰੀ" + "External": "ਬਾਹਰੀ", + "HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index eff0196d7..3eb1e0468 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Последние добавленные", + "Latest": "Последние", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 3cdf743d5..da32e9776 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -123,5 +123,7 @@ "External": "ภายนอก", "HearingImpaired": "บกพร่องทางการได้ยิน", "TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม", - "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน" + "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน", + "TaskRefreshTrickplayImages": "สร้างไฟล์รูปภาพสำหรับ Trickplay", + "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay" } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs index df0fdcab8..301c04915 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs @@ -29,7 +29,7 @@ public partial class AudioNormalizationTask : IScheduledTask private readonly IItemRepository _itemRepository; private readonly ILibraryManager _libraryManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IConfigurationManager _configurationManager; + private readonly IApplicationPaths _applicationPaths; private readonly ILocalizationManager _localization; private readonly ILogger<AudioNormalizationTask> _logger; @@ -39,21 +39,21 @@ public partial class AudioNormalizationTask : IScheduledTask /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param> public AudioNormalizationTask( IItemRepository itemRepository, ILibraryManager libraryManager, IMediaEncoder mediaEncoder, - IConfigurationManager configurationManager, + IApplicationPaths applicationPaths, ILocalizationManager localizationManager, ILogger<AudioNormalizationTask> logger) { _itemRepository = itemRepository; _libraryManager = libraryManager; _mediaEncoder = mediaEncoder; - _configurationManager = configurationManager; + _applicationPaths = applicationPaths; _localization = localizationManager; _logger = logger; } @@ -107,7 +107,9 @@ public partial class AudioNormalizationTask : IScheduledTask } _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); - var tempFile = Path.Join(_configurationManager.GetTranscodePath(), a.Id + ".concat"); + var tempDir = _applicationPaths.TempDirectory; + Directory.CreateDirectory(tempDir); + var tempFile = Path.Join(tempDir, a.Id + ".concat"); var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); try diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 6cb06d31c..254500ccd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -62,16 +62,17 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <inheritdoc /> public bool IsLogged => true; - /// <summary> - /// Creates the triggers that define when the task will run. - /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + /// <inheritdoc /> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { return new[] { new TaskTriggerInfo { + Type = TaskTriggerInfo.TriggerStartup + }, + new TaskTriggerInfo + { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index d65ac2e5e..9425b47d0 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -27,45 +27,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers TaskOptions = taskOptions; } - /// <summary> - /// Occurs when [triggered]. - /// </summary> + /// <inheritdoc /> public event EventHandler<EventArgs>? Triggered; - /// <summary> - /// Gets the options of this task. - /// </summary> + /// <inheritdoc /> public TaskOptions TaskOptions { get; } - /// <summary> - /// Stars waiting for the trigger action. - /// </summary> - /// <param name="lastResult">The last result.</param> - /// <param name="logger">The logger.</param> - /// <param name="taskName">The name of the task.</param> - /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> + /// <inheritdoc /> public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) { DisposeTimer(); + DateTime now = DateTime.UtcNow; DateTime triggerDate; if (lastResult is null) { // Task has never been completed before - triggerDate = DateTime.UtcNow.AddHours(1); + triggerDate = now.AddHours(1); } else { - triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval); - } - - if (DateTime.UtcNow > triggerDate) - { - triggerDate = DateTime.UtcNow.AddMinutes(1); + triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate, now.AddMinutes(1) }.Max().Add(_interval); } - var dueTime = triggerDate - DateTime.UtcNow; + var dueTime = triggerDate - now; var maxDueTime = TimeSpan.FromDays(7); if (dueTime > maxDueTime) @@ -76,9 +62,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); } - /// <summary> - /// Stops waiting for the trigger action. - /// </summary> + /// <inheritdoc /> public void Stop() { DisposeTimer(); diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 69c402f63..bb32b7c20 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -231,7 +231,7 @@ public class TrickplayManager : ITrickplayManager throw new ArgumentException("Can't create trickplay from 0 images."); } - var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")); + var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(workDir); var trickplayInfo = new TrickplayInfo diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index 5311a30e4..5518d6ba8 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -60,6 +60,7 @@ public static class StartupHelpers logger.LogInformation("Log directory path: {LogDirectoryPath}", appPaths.LogDirectoryPath); logger.LogInformation("Config directory path: {ConfigurationDirectoryPath}", appPaths.ConfigurationDirectoryPath); logger.LogInformation("Cache path: {CachePath}", appPaths.CachePath); + logger.LogInformation("Temp directory path: {TempDirPath}", appPaths.TempDirectory); logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath); logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8bd4fb4f3..68ae67d05 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2497,11 +2497,6 @@ namespace MediaBrowser.Controller.Entities return new[] { Id }; } - public virtual List<ExternalUrl> GetRelatedUrls() - { - return new List<ExternalUrl>(); - } - public virtual double? GetRefreshProgress() { return null; diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index ede544eec..710b05e7f 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -121,23 +121,5 @@ namespace MediaBrowser.Controller.Entities.Movies return hasChanges; } - - /// <inheritdoc /> - public override List<ExternalUrl> GetRelatedUrls() - { - var list = base.GetRelatedUrls(); - - var imdbId = this.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdbId)) - { - list.Add(new ExternalUrl - { - Name = "Trakt", - Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) - }); - } - - return list; - } } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 37e241414..5c54f014c 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -344,22 +344,5 @@ namespace MediaBrowser.Controller.Entities.TV return hasChanges; } - - public override List<ExternalUrl> GetRelatedUrls() - { - var list = base.GetRelatedUrls(); - - var imdbId = this.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdbId)) - { - list.Add(new ExternalUrl - { - Name = "Trakt", - Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/episodes/{0}", imdbId) - }); - } - - return list; - } } } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 6297b67e4..69b04a927 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -482,22 +482,5 @@ namespace MediaBrowser.Controller.Entities.TV return hasChanges; } - - public override List<ExternalUrl> GetRelatedUrls() - { - var list = base.GetRelatedUrls(); - - var imdbId = this.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdbId)) - { - list.Add(new ExternalUrl - { - Name = "Trakt", - Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId) - }); - } - - return list; - } } } diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 81d50bbc1..939709215 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -80,22 +80,5 @@ namespace MediaBrowser.Controller.Entities return hasChanges; } - - public override List<ExternalUrl> GetRelatedUrls() - { - var list = base.GetRelatedUrls(); - - var imdbId = this.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdbId)) - { - list.Add(new ExternalUrl - { - Name = "Trakt", - Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) - }); - } - - return list; - } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 05540d490..2ac6f9963 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -254,25 +254,5 @@ namespace MediaBrowser.Controller.LiveTv return name; } - - public override List<ExternalUrl> GetRelatedUrls() - { - var list = base.GetRelatedUrls(); - - var imdbId = this.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdbId)) - { - if (IsMovie) - { - list.Add(new ExternalUrl - { - Name = "Trakt", - Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) - }); - } - } - - return list; - } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9d7d2fd12..b175dc403 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -276,6 +276,21 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.SupportsFilter("scale_vt"); } + private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) + { + if (state.VideoStream is null + || !options.EnableTonemapping + || GetVideoColorBitDepth(state) != 10 + || !_mediaEncoder.SupportsFilter("tonemapx") + || !(string.Equals(state.VideoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + return state.VideoStream.VideoRange == VideoRange.HDR + && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; + } + private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream is null @@ -1188,10 +1203,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat"); - _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); + var concatFilePath = Path.Join(_configurationManager.CommonApplicationPaths.CachePath, "concat", state.MediaSource.Id + ".concat"); + if (!File.Exists(concatFilePath)) + { + _mediaEncoder.GenerateConcatConfig(state.MediaSource, concatFilePath); + } + arg.Append(" -f concat -safe 0 -i \"") - .Append(tmpConcatPath) + .Append(concatFilePath) .Append("\" "); } else @@ -3517,6 +3536,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; + var doToneMap = IsSwTonemapAvailable(state, options); var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; @@ -3525,7 +3545,7 @@ namespace MediaBrowser.Controller.MediaEncoding /* Make main filters for video stream */ var mainFilters = new List<string>(); - mainFilters.Add(GetOverwriteColorPropertiesParam(state, false)); + mainFilters.Add(GetOverwriteColorPropertiesParam(state, doToneMap)); // INPUT sw surface(memory/copy-back from vram) // sw deint @@ -3548,11 +3568,31 @@ namespace MediaBrowser.Controller.MediaEncoding // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); - // sw tonemap <= TODO: finsh the fast tonemap filter + // sw tonemap <= TODO: finish dovi tone mapping + + if (doToneMap) + { + var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={outFormat}"; - // OUTPUT yuv420p/nv12 surface(memory) + if (options.TonemappingParam != 0) + { + tonemapArgs += $":param={options.TonemappingParam}"; + } + + if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) + || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + { + tonemapArgs += $":range={options.TonemappingRange}"; + } + + mainFilters.Add(tonemapArgs); + } + else + { + // OUTPUT yuv420p/nv12 surface(memory) + mainFilters.Add("format=" + outFormat); + } /* Make sub and overlay filters for subtitle stream */ var subFilters = new List<string>(); diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs index 0d847520d..f451eac6d 100644 --- a/MediaBrowser.Controller/Providers/IExternalId.cs +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -1,3 +1,4 @@ +using System; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; @@ -33,6 +34,7 @@ namespace MediaBrowser.Controller.Providers /// <summary> /// Gets the URL format string for this id. /// </summary> + [Obsolete("Obsolete in 10.10, to be removed in 10.11")] string? UrlFormatString { get; } /// <summary> diff --git a/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs b/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs new file mode 100644 index 000000000..86a180627 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Providers; + +/// <summary> +/// Interface to include related urls for an item. +/// </summary> +public interface IExternalUrlProvider +{ + /// <summary> + /// Gets the external service name. + /// </summary> + string Name { get; } + + /// <summary> + /// Get the list of external urls. + /// </summary> + /// <param name="item">The item to get external urls for.</param> + /// <returns>The list of external urls.</returns> + IEnumerable<string> GetExternalUrls(BaseItem item); +} diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index b52f16edc..38fc5f2cc 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -99,12 +99,14 @@ namespace MediaBrowser.Controller.Providers /// <param name="metadataProviders">Metadata providers to use.</param> /// <param name="metadataSavers">Metadata savers to use.</param> /// <param name="externalIds">External IDs to use.</param> + /// <param name="externalUrlProviders">The list of external url providers.</param> void AddParts( IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers, - IEnumerable<IExternalId> externalIds); + IEnumerable<IExternalId> externalIds, + IEnumerable<IExternalUrlProvider> externalUrlProviders); /// <summary> /// Gets the available remote images. diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index 8ebb59c59..5bae4fbd5 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -86,7 +86,7 @@ public class BdInfoExaminer : IBlurayExaminer if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0) { // Get the files in the playlist - outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray(); + outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.FileInfo.FullName).ToArray(); } return outputStream; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index ec39f159e..30bb21dcb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -103,6 +103,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // sw "alphasrc", "zscale", + "tonemapx", // qsv "scale_qsv", "vpp_qsv", diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 390891fcf..de1b65482 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -708,16 +708,22 @@ namespace MediaBrowser.MediaEncoding.Encoder filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24")); } - // Use SW tonemap on HDR10/HLG video stream only when the zscale filter is available. + // Use SW tonemap on HDR10/HLG video stream only when the zscale or tonemapx filter is available. var enableHdrExtraction = false; - if ((string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + if (string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) || string.Equals(videoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) - && SupportsFilter("zscale")) { - enableHdrExtraction = true; - - filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"); + if (SupportsFilter("tonemapx")) + { + enableHdrExtraction = true; + filters.Add("tonemapx=tonemap=bt2390:desat=0:peak=100:t=bt709:m=bt709:p=bt709:format=yuv420p"); + } + else if (SupportsFilter("zscale")) + { + enableHdrExtraction = true; + filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"); + } } var vf = string.Join(',', filters); @@ -1170,18 +1176,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path) - { - // Get all playable .m2ts files - var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; - - // Get all files from the BDMV/STREAMING directory - // Only return playable local .m2ts files - var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList(); - return validPlaybackFiles - .Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName) - .Where(f => f is not null) - .ToList(); - } + => _blurayExaminer.GetDiscInfo(path).Files; /// <inheritdoc /> public string GetInputPathArgument(EncodingJobInfo state) @@ -1192,8 +1187,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { return mediaSource.VideoType switch { - VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null).ToList(), mediaSource), - VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path).ToList(), mediaSource), + VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource), + VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource), _ => GetInputArgument(path, mediaSource) }; } @@ -1218,6 +1213,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Generate concat configuration entries for each file and write to file + Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath)); using StreamWriter sw = new StreamWriter(concatFilePath); foreach (var path in files) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 8b2685fe1..f5185398c 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1319,23 +1319,38 @@ namespace MediaBrowser.MediaEncoding.Probing // These support multiple values, but for now we only store the first. var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID")); - audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb); + if (!string.IsNullOrEmpty(mb)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb); + } mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID")); - audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb); + if (!string.IsNullOrEmpty(mb)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb); + } mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID")); - audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb); + if (!string.IsNullOrEmpty(mb)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb); + } mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID")); - audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb); + if (!string.IsNullOrEmpty(mb)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb); + } mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID")); - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb); + if (!string.IsNullOrEmpty(mb)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb); + } } private string GetMultipleMusicBrainzId(string value) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 16837d98b..9ecbfa9cf 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -198,10 +198,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { - await ExtractAllTextSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); + await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); - var outputFormat = GetTextSubtitleFormat(subtitleStream); - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat); + var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); + var outputFormat = GetExtractableSubtitleFormat(subtitleStream); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); return new SubtitleInfo() { @@ -215,6 +216,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) .TrimStart('.'); + // Handle PGS subtitles as raw streams for the client to render + if (MediaStream.IsPgsFormat(currentFormat)) + { + return new SubtitleInfo() + { + Path = subtitleStream.Path, + Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path), + Format = "pgssub", + IsExternal = true + }; + } + // Fallback to ffmpeg conversion if (!_subtitleParser.SupportsFileExtension(currentFormat)) { @@ -428,10 +441,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); } - private string GetTextSubtitleFormat(MediaStream subtitleStream) + private string GetExtractableSubtitleFormat(MediaStream subtitleStream) { if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)) + || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase)) { return subtitleStream.Codec; } @@ -441,21 +455,35 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + private string GetExtractableSubtitleFileExtension(MediaStream subtitleStream) + { + // Using .pgssub as file extension is not allowed by ffmpeg. The file extension for pgs subtitles is .sup. + if (string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase)) + { + return "sup"; + } + else + { + return GetExtractableSubtitleFormat(subtitleStream); + } + } + private bool IsCodecCopyable(string codec) { return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase); + || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase); } /// <summary> - /// Extracts all text subtitles. + /// Extracts all extractable subtitles (text and pgs). /// </summary> /// <param name="mediaSource">The mediaSource.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) + private async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) { var locks = new List<IDisposable>(); var extractableStreams = new List<MediaStream>(); @@ -463,11 +491,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { var subtitleStreams = mediaSource.MediaStreams - .Where(stream => stream is { IsTextSubtitleStream: true, SupportsExternalStream: true, IsExternal: false }); + .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true, IsExternal: false }); foreach (var subtitleStream in subtitleStreams) { - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); @@ -483,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (extractableStreams.Count > 0) { - await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); + await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) @@ -496,7 +524,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private async Task ExtractAllTextSubtitlesInternal( + private async Task ExtractAllExtractableSubtitlesInternal( MediaSourceInfo mediaSource, List<MediaStream> subtitleStreams, CancellationToken cancellationToken) @@ -510,7 +538,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles foreach (var subtitleStream in subtitleStreams) { - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index ea5dbf7f7..0b09e57b5 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -235,15 +235,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable if (delete(job.Path!)) { await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); - if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay) - { - var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat"); - if (File.Exists(concatFilePath)) - { - _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath); - File.Delete(concatFilePath); - } - } } if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId)) @@ -419,7 +410,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var concatPath = Path.Join(_serverConfigurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat"); + var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat"); await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); } else diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index e1082adea..dcb3febbd 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Dlna; @@ -585,6 +586,33 @@ namespace MediaBrowser.Model.Entities } } + [JsonIgnore] + public bool IsPgsSubtitleStream + { + get + { + if (Type != MediaStreamType.Subtitle) + { + return false; + } + + if (string.IsNullOrEmpty(Codec) && !IsExternal) + { + return false; + } + + return IsPgsFormat(Codec); + } + } + + /// <summary> + /// Gets a value indicating whether this is a subtitle steam that is extractable by ffmpeg. + /// All text-based and pgs subtitles can be extracted. + /// </summary> + /// <value><c>true</c> if this is a extractable subtitle steam otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream; + /// <summary> /// Gets or sets a value indicating whether [supports external stream]. /// </summary> @@ -666,6 +694,14 @@ namespace MediaBrowser.Model.Entities && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)); } + public static bool IsPgsFormat(string format) + { + string codec = format ?? string.Empty; + + return codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase); + } + public bool SupportsSubtitleConversionTo(string toCodec) { if (!IsTextSubtitleStream) diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index cf453d62c..1c73091f0 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -111,31 +111,32 @@ namespace MediaBrowser.Model.Entities /// Sets a provider id. /// </summary> /// <param name="instance">The instance.</param> - /// <param name="name">The name.</param> + /// <param name="name">The name, this should not contain a '=' character.</param> /// <param name="value">The value.</param> - public static void SetProviderId(this IHasProviderIds instance, string name, string? value) + /// <remarks>Due to how deserialization from the database works the name can not contain '='.</remarks> + public static void SetProviderId(this IHasProviderIds instance, string name, string value) { ArgumentNullException.ThrowIfNull(instance); + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentException.ThrowIfNullOrEmpty(value); + + // When name contains a '=' it can't be deserialized from the database + if (name.Contains('=', StringComparison.Ordinal)) + { + throw new ArgumentException("Provider id name cannot contain '='", nameof(name)); + } + + // Ensure it exists + instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - // If it's null remove the key from the dictionary - if (string.IsNullOrEmpty(value)) + // Match on internal MetadataProvider enum string values before adding arbitrary providers + if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue)) { - instance.ProviderIds?.Remove(name); + instance.ProviderIds[enumValue] = value; } else { - // Ensure it exists - instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // Match on internal MetadataProvider enum string values before adding arbitrary providers - if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue)) - { - instance.ProviderIds[enumValue] = value; - } - else - { - instance.ProviderIds[name] = value; - } + instance.ProviderIds[name] = value; } } @@ -149,5 +150,30 @@ namespace MediaBrowser.Model.Entities { instance.SetProviderId(provider.ToString(), value); } + + /// <summary> + /// Removes a provider id. + /// </summary> + /// <param name="instance">The instance.</param> + /// <param name="name">The name.</param> + public static void RemoveProviderId(this IHasProviderIds instance, string name) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentException.ThrowIfNullOrEmpty(name); + + instance.ProviderIds?.Remove(name); + } + + /// <summary> + /// Removes a provider id. + /// </summary> + /// <param name="instance">The instance.</param> + /// <param name="provider">The provider.</param> + public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider) + { + ArgumentNullException.ThrowIfNull(instance); + + instance.ProviderIds?.Remove(provider.ToString()); + } } } diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs index d026d574f..1f5163aa8 100644 --- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs +++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace MediaBrowser.Model.Providers { /// <summary> @@ -17,7 +19,9 @@ namespace MediaBrowser.Model.Providers Name = name; Key = key; Type = type; +#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 UrlFormatString = urlFormatString; +#pragma warning restore CS0618 // Type or member is obsolete } /// <summary> @@ -46,6 +50,7 @@ namespace MediaBrowser.Model.Providers /// <summary> /// Gets or sets the URL format string. /// </summary> + [Obsolete("Obsolete in 10.10, to be removed in 10.11")] public string? UrlFormatString { get; set; } } } diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs index f8a8c727e..1d8767dc1 100644 --- a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs +++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs @@ -13,7 +13,6 @@ namespace MediaBrowser.Model.Tasks public const string TriggerDaily = "DailyTrigger"; public const string TriggerWeekly = "WeeklyTrigger"; public const string TriggerInterval = "IntervalTrigger"; - public const string TriggerSystemEvent = "SystemEventTrigger"; public const string TriggerStartup = "StartupTrigger"; /// <summary> diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f2ca99da6..60d89a51b 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -69,11 +69,12 @@ namespace MediaBrowser.Providers.Manager o.PoolInitialFill = 1; }); - private IImageProvider[] _imageProviders = Array.Empty<IImageProvider>(); - private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>(); - private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>(); - private IMetadataSaver[] _savers = Array.Empty<IMetadataSaver>(); - private IExternalId[] _externalIds = Array.Empty<IExternalId>(); + private IImageProvider[] _imageProviders = []; + private IMetadataService[] _metadataServices = []; + private IMetadataProvider[] _metadataProviders = []; + private IMetadataSaver[] _savers = []; + private IExternalId[] _externalIds = []; + private IExternalUrlProvider[] _externalUrlProviders = []; private bool _isProcessingRefreshQueue; private bool _disposed; @@ -132,12 +133,14 @@ namespace MediaBrowser.Providers.Manager IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers, - IEnumerable<IExternalId> externalIds) + IEnumerable<IExternalId> externalIds, + IEnumerable<IExternalUrlProvider> externalUrlProviders) { _imageProviders = imageProviders.ToArray(); _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray(); _metadataProviders = metadataProviders.ToArray(); _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray(); + _externalUrlProviders = externalUrlProviders.OrderBy(i => i.Name).ToArray(); _savers = metadataSavers.ToArray(); } @@ -877,31 +880,35 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item) { - return GetExternalIds(item) +#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 + var legacyExternalIdUrls = GetExternalIds(item) .Select(i => - { - if (string.IsNullOrEmpty(i.UrlFormatString)) { - return null; - } + var urlFormatString = i.UrlFormatString; + if (string.IsNullOrEmpty(urlFormatString) + || !item.TryGetProviderId(i.Key, out var providerId)) + { + return null; + } - var value = item.GetProviderId(i.Key); + return new ExternalUrl + { + Name = i.ProviderName, + Url = string.Format( + CultureInfo.InvariantCulture, + urlFormatString, + providerId) + }; + }) + .OfType<ExternalUrl>(); +#pragma warning restore CS0618 // Type or member is obsolete - if (string.IsNullOrEmpty(value)) - { - return null; - } + var externalUrls = _externalUrlProviders + .SelectMany(p => p + .GetExternalUrls(item) + .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl })); - return new ExternalUrl - { - Name = i.ProviderName, - Url = string.Format( - CultureInfo.InvariantCulture, - i.UrlFormatString, - value) - }; - }).Where(i => i is not null) - .Concat(item.GetRelatedUrls())!; // We just filtered out all the nulls + return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name); } /// <inheritdoc/> @@ -912,7 +919,9 @@ namespace MediaBrowser.Providers.Manager name: i.ProviderName, key: i.Key, type: i.Type, +#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 urlFormatString: i.UrlFormatString)); +#pragma warning restore CS0618 // Type or member is obsolete } /// <inheritdoc/> diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 9eacfc2b6..0083d4f75 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -325,22 +325,26 @@ namespace MediaBrowser.Providers.MediaInfo audio.NormalizationGain = (float)tags.ReplayGainTrackGain; } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) { audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) { audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) { audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) { audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 1d4e66570..246ba2733 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -124,11 +124,8 @@ namespace MediaBrowser.Providers.MediaInfo // Get BD disc information blurayDiscInfo = GetBDInfo(item.Path); - // Get playable .m2ts files - var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); - // Return if no playable .m2ts files are found - if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0) { _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); return ItemUpdateType.MetadataImport; @@ -138,7 +135,7 @@ namespace MediaBrowser.Providers.MediaInfo mediaInfoResult = await GetMediaInfo( new Video { - Path = m2ts[0] + Path = blurayDiscInfo.Files[0] }, cancellationToken).ConfigureAwait(false); } diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 75963226a..ede93aaa5 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -219,7 +219,7 @@ public class SkiaEncoder : IImageEncoder return path; } - var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); + var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); Directory.CreateDirectory(directory); File.Copy(path, tempPath, true); diff --git a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs index 2a62ab74c..a6f416414 100644 --- a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs @@ -141,7 +141,7 @@ namespace Jellyfin.Model.Tests.Entities public void SetProviderId_Null_Remove() { var provider = new ProviderIdsExtensionsTestsObject(); - provider.SetProviderId(MetadataProvider.Imdb, null!); + Assert.Throws<ArgumentNullException>(() => provider.SetProviderId(MetadataProvider.Imdb, null!)); Assert.Empty(provider.ProviderIds); } @@ -150,8 +150,8 @@ namespace Jellyfin.Model.Tests.Entities { var provider = new ProviderIdsExtensionsTestsObject(); provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId; - provider.SetProviderId(MetadataProvider.Imdb, string.Empty); - Assert.Empty(provider.ProviderIds); + Assert.Throws<ArgumentException>(() => provider.SetProviderId(MetadataProvider.Imdb, string.Empty)); + Assert.Single(provider.ProviderIds); } [Fact] @@ -182,10 +182,20 @@ namespace Jellyfin.Model.Tests.Entities ProviderIds = null! }; - nullProvider.SetProviderId(MetadataProvider.Imdb, string.Empty); + Assert.Throws<ArgumentException>(() => nullProvider.SetProviderId(MetadataProvider.Imdb, string.Empty)); Assert.Null(nullProvider.ProviderIds); } + [Fact] + public void RemoveProviderId_Null_Remove() + { + var provider = new ProviderIdsExtensionsTestsObject(); + + provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId; + provider.RemoveProviderId(MetadataProvider.Imdb); + Assert.Empty(provider.ProviderIds); + } + private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds { public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject(); diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index 6fccce049..cced2b1e2 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -585,15 +585,17 @@ namespace Jellyfin.Providers.Tests.Manager IEnumerable<IMetadataService>? metadataServices = null, IEnumerable<IMetadataProvider>? metadataProviders = null, IEnumerable<IMetadataSaver>? metadataSavers = null, - IEnumerable<IExternalId>? externalIds = null) + IEnumerable<IExternalId>? externalIds = null, + IEnumerable<IExternalUrlProvider>? externalUrlProviders = null) { imageProviders ??= Array.Empty<IImageProvider>(); metadataServices ??= Array.Empty<IMetadataService>(); metadataProviders ??= Array.Empty<IMetadataProvider>(); metadataSavers ??= Array.Empty<IMetadataSaver>(); externalIds ??= Array.Empty<IExternalId>(); + externalUrlProviders ??= Array.Empty<IExternalUrlProvider>(); - providerManager.AddParts(imageProviders, metadataServices, metadataProviders, metadataSavers, externalIds); + providerManager.AddParts(imageProviders, metadataServices, metadataProviders, metadataSavers, externalIds, externalUrlProviders); } /// <summary> |
