aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-compat.yml149
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props12
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json82
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json4
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs206
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs36
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs23
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs29
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs6
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs123
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs125
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs96
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs45
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u81
31 files changed, 712 insertions, 286 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index b71b365f7..cfb5a6ec2 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.9+
+ - 10.9.10+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
new file mode 100644
index 000000000..c6e655d08
--- /dev/null
+++ b/.github/workflows/ci-compat.yml
@@ -0,0 +1,149 @@
+name: ABI Compatibility
+on:
+ pull_request_target:
+
+permissions: {}
+
+jobs:
+ abi-head:
+ name: ABI - HEAD
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+
+ - name: Build
+ run: |
+ dotnet build Jellyfin.Server -o ./out
+
+ - name: Upload Head
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
+ with:
+ name: abi-head
+ retention-days: 14
+ if-no-files-found: error
+ path: out/
+
+ abi-base:
+ name: ABI - BASE
+ if: ${{ github.base_ref != '' }}
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ fetch-depth: 0
+
+ - name: Checkout common ancestor
+ env:
+ HEAD_REF: ${{ github.head_ref }}
+ run: |
+ git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
+ git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
+ ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
+ git checkout --progress --force $ANCESTOR_REF
+
+ - name: Build
+ run: |
+ dotnet build Jellyfin.Server -o ./out
+
+ - name: Upload Head
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
+ with:
+ name: abi-base
+ retention-days: 14
+ if-no-files-found: error
+ path: out/
+
+ abi-diff:
+ permissions:
+ pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
+
+ name: ABI - Difference
+ if: ${{ github.event_name == 'pull_request_target' }}
+ runs-on: ubuntu-latest
+ needs:
+ - abi-head
+ - abi-base
+
+ steps:
+ - name: Download abi-head
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ with:
+ name: abi-head
+ path: abi-head
+
+ - name: Download abi-base
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ with:
+ name: abi-base
+ path: abi-base
+
+ - name: Setup ApiCompat
+ run: |
+ dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
+
+ - name: Run ApiCompat
+ id: diff
+ run: |
+ {
+ echo 'body<<EOF'
+ for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll; do
+ COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
+ if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
+ printf "\n${file}\n${COMPAT_OUTPUT}\n"
+ fi
+ done
+ echo EOF
+ } >> $GITHUB_OUTPUT
+
+ - name: Find difference comment
+ uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
+ id: find-comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ direction: last
+ body-includes: abi-diff-workflow-comment
+
+ - name: Reply or edit difference comment (changed)
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
+ if: ${{ steps.diff.outputs.body != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ body: |
+ <!--abi-diff-workflow-comment-->
+ <details>
+ <summary>ABI Difference</summary>
+
+ ```
+ ${{ steps.diff.outputs.body }}
+ ```
+
+ </details>
+
+ - name: Reply or edit difference comment (unchanged)
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
+ if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ body: |
+ <!--abi-diff-workflow-comment-->
+ <details>
+ <summary>ABI Difference</summary>
+
+ No changes to the ABI found. See history of this comment for previous changes.
+
+ </details>
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 93bfc83a2..d01c506db 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -27,7 +27,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: openapi-head
retention-days: 14
@@ -61,7 +61,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: openapi-base
retention-days: 14
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 91c2be87b..af8106c0a 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@5808021ec4deecb0ab3da051d49b4ce65fcc20af # 5.3.8
+ uses: danielpalme/ReportGenerator-GitHub-Action@e3af7259842d9c814021ea121f85526e0872b25f # v5.3.9
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 7c2e72327..cdf8df17f 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -187,6 +187,7 @@
- [HonestlyWhoKnows](https://github.com/honestlywhoknows)
- [TheMelmacian](https://github.com/TheMelmacian)
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
+ - [pret0rian8](https://github.com/pret0rian)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a153e1d5f..02937b193 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,8 +9,8 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
- <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.2" />
- <PackageVersion Include="BlurHashSharp" Version="1.3.2" />
+ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
+ <PackageVersion Include="BlurHashSharp" Version="1.3.3" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
@@ -21,8 +21,8 @@
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="4.0.7" />
- <PackageVersion Include="LrcParser" Version="2023.524.0" />
+ <PackageVersion Include="libse" Version="4.0.8" />
+ <PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
@@ -47,7 +47,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -80,7 +80,7 @@
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.1.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.3.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 6d71e99a1..48d24385e 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -669,7 +669,7 @@ namespace Emby.Server.Implementations.Library
if (parent is not null)
{
- var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>().ToArray();
+ var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>();
foreach (var resolver in multiItemResolvers)
{
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index e871a4362..69217dba0 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -93,7 +93,7 @@
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
- "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
+ "TaskDownloadMissingSubtitles": "Hentede medie mangler undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
- "TaskAudioNormalization": "Audio-normalisering"
+ "TaskAudioNormalization": "Audio-normalisering",
+ "TaskDownloadMissingLyricsDescription": "Hentede sange mangler sangtekster",
+ "TaskDownloadMissingLyrics": "Hentede medie mangler sangtekster"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index ce98979e6..bbb162c77 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
"TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
- "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten."
+ "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
+ "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
+ "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 9433da28b..b926d9d30 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -130,5 +130,7 @@
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
- "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
+ "TaskDownloadMissingLyrics": "Descargar letra faltante",
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 13e007b4c..210ee4446 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskAudioNormalization": "Normalización de audio",
- "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización."
+ "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
+ "TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
+ "TaskDownloadMissingLyrics": "Descargar letras faltantes"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index e7deefbb0..b458ed423 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -129,5 +129,7 @@
"TaskAudioNormalization": "Normalización de audio",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
- "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción"
+ "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
+ "TaskDownloadMissingLyrics": "Descargar letra faltante",
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index ce5177d1f..b0ddec104 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -128,5 +128,9 @@
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
"TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
- "TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند."
+ "TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند.",
+ "TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.",
+ "TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود",
+ "TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها",
+ "TaskAudioNormalization": "نرمال کردن صدا"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 0e694af02..961d1a0df 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio",
- "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio."
+ "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
+ "TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
+ "TaskDownloadMissingLyrics": "Scarica testi mancanti"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
index d6ff58785..ffb4345c8 100644
--- a/Emby.Server.Implementations/Localization/Core/kw.json
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -26,13 +26,13 @@
"NotificationOptionPluginUninstalled": "Ystynnans anynstallys",
"NotificationOptionPluginUpdateInstalled": "Nowedheans ystynnans ynstallys",
"Application": "Gweythres",
- "Favorites": "Drudh",
+ "Favorites": "Moyha Kerys",
"Forced": "Konstrynys",
"Albums": "Albomow",
"Books": "Lyvrow",
- "Channels": "Gothi",
+ "Channels": "Kanolyow",
"AppDeviceValues": "App: {0}, Devis: {1}",
- "Artists": "Artydhyon",
+ "Artists": "Artyhdyon",
"HeaderAlbumArtists": "Albom artydhyon",
"HeaderNextUp": "Nessa",
"CameraImageUploadedFrom": "Skeusen kamera nowydh re beu ughkargys a-dhyworth {0}",
@@ -44,7 +44,7 @@
"ItemRemovedWithName": "{0} a veu dileys a-dhyworth an lyverva",
"LabelIpAddressValue": "Trigva PK: {)}",
"Music": "Ilow",
- "HeaderContinueWatching": "Pesya ow kweles",
+ "HeaderContinueWatching": "Pesya Ow Kweles",
"NameSeasonNumber": "Seson {0}",
"NotificationOptionApplicationUpdateInstalled": "Nowedheans gweythres ynstallys",
"NotificationOptionCameraImageUploaded": "Skeusen kamera ughkargys",
@@ -59,5 +59,77 @@
"NotificationOptionInstallationFailed": "Defowt ynstallyans",
"Genres": "Eghennow",
"NotificationOptionPluginInstalled": "Ystynnans ynstallys",
- "NotificationOptionServerRestartRequired": "Dastalleth servell yw res"
+ "NotificationOptionServerRestartRequired": "Dastalleth servell yw res",
+ "StartupEmbyServerIsLoading": "Yma Servell Jellyfin ow kargya. Assay arta yn berr mar pleg.",
+ "SubtitleDownloadFailureFromForItem": "Istitlow a fyllis iskarga a-dhyworth {0] rag {1}",
+ "System": "Kevreyth",
+ "User": "Devnydhyer",
+ "UserDeletedWithName": "Devnydhyer {0} re beu dileys",
+ "UserLockedOutWithName": "Devnydhyer {0} re beu alhwedhys yn-mes",
+ "UserStoppedPlayingItemWithValues": "{0} re worfennas gwari {1} war {2}",
+ "UserOfflineFromDevice": "{0} re anjunyas a-dhyworth {1}",
+ "UserOnlineFromDevice": "{0} yw warlinen a-dhyworth {1}",
+ "NotificationOptionUserLockedOut": "Devnydhyer yw alhwedhys yn-mes",
+ "Photos": "Skeusennow",
+ "Playlists": "Rolyow-gwari",
+ "Plugin": "Ystynnans",
+ "PluginInstalledWithName": "{0} a veu ynstallys",
+ "UserPolicyUpdatedWithName": "Polici devnydhyer re beu nowedhys rag {0}",
+ "PluginUpdatedWithName": "{0} a veu nowedhys",
+ "ScheduledTaskFailedWithName": "{0} a fyllis",
+ "Songs": "Kanow",
+ "Sync": "Kesseni",
+ "TvShows": "Towlennow PW",
+ "Undefined": "Anstyrys",
+ "UserCreatedWithName": "Devnydhyer {0} re beu gwruthys",
+ "UserDownloadingItemWithValues": "Yma {0} owth iskarga {1}",
+ "UserPasswordChangedWithName": "Ger-tremena re beu chanjys rag devnydhyer {0}",
+ "UserStartedPlayingItemWithValues": "Yma {0} ow kwari {1} war {2}",
+ "ValueHasBeenAddedToLibrary": "{0} re beu keworrys dhe'th lyverva media",
+ "VersionNumber": "Versyon {0}",
+ "TasksLibraryCategory": "Lyverva",
+ "TaskCleanActivityLog": "Glanhe Kovlyver Gwrians",
+ "TaskRefreshPeople": "Disegha Tus",
+ "TaskRefreshLibrary": "Arhwilas Lyverva Media",
+ "TaskCleanTranscodeDescription": "Y hwra dilea restrennow treylya neg a veu gwrys kyns nans yw dydh.",
+ "NotificationOptionVideoPlaybackStopped": "Gwareans gwydhyow yw hedhys",
+ "NotificationOptionVideoPlayback": "Gwareans gwydhyow yw dallethys",
+ "PluginUninstalledWithName": "{0} a veu anynstallys",
+ "NotificationOptionTaskFailed": "Defowt oberen towlennys",
+ "ProviderValue": "Provier: {0}",
+ "ScheduledTaskStartedWithName": "{0} a dhallathas",
+ "ServerNameNeedsToBeRestarted": "Yma edhom dhe {0} a vos dastallathys",
+ "ValueSpecialEpisodeName": "Arbennik - {0}",
+ "TasksMaintenanceCategory": "Mentons",
+ "TasksApplicationCategory": "Gweythres",
+ "TasksChannelsCategory": "Kanolyow Kesrosweyth",
+ "TaskCleanLogs": "Glanhe Kevarwodhyador Kovlyver",
+ "TaskAudioNormalization": "Normalheans Klewans",
+ "TaskRefreshChannels": "Disegha Kanolyow",
+ "TaskCleanTranscode": "Glanhe Kevarwodhyador Treylya",
+ "TaskUpdatePlugins": "Nowedhi Ystynansow",
+ "Shows": "Diskwedhyansow",
+ "TaskCleanCache": "Glanhe Kevarwodhyador Gwithva",
+ "TaskCleanActivityLogDescription": "Y hwra dilea lin kovlyver gwrians kottha ages an bloodh dewisys.",
+ "TaskCleanCacheDescription": "Y hwra dilea restrennow gwithva nag yw res rag an kevreyth.",
+ "TaskRefreshPeopleDescription": "Y hwra nowedhi metadata rag gwarioryon ha kevarwodhoryon yn dha lyverva media.",
+ "TaskRefreshChapterImages": "Kuntel Imajys Chaptra",
+ "TaskRefreshChapterImagesDescription": "Y hwra ewines meus rag gwydhyowyow gans chaptraow.",
+ "TaskRefreshTrickplayImagesDescription": "Y hwra kynwelyow trickplay rag gwydhyowyow yn lyvervaow gallosegys.",
+ "TaskRefreshTrickplayImages": "Dinythi Imajys Trickplay",
+ "TaskCleanLogsDescription": "Y hwra dilea restrennow kovlyver a veu gwrys kyns nans yw {0} dydh.",
+ "TaskDownloadMissingLyrics": "Iskarga geryow kellys",
+ "TaskUpdatePluginsDescription": "Y hwra iskarga hag ynstallya nowedheansow rag ystynansow neb yw dewisys dhe nowedhi yn awtomatek.",
+ "TaskDownloadMissingSubtitles": "Iskarga istitlow kellys",
+ "TaskRefreshChannelsDescription": "Y hwra disegha kedhlow kanolyow kesrosweyth.",
+ "TaskDownloadMissingLyricsDescription": "Y hwra iskarga geryow rag kanow",
+ "TaskDownloadMissingSubtitlesDescription": "Y hwra hwilas an kesrosweyth rag istitlow kellys a-dhywoth dewisyans metadata.",
+ "TaskOptimizeDatabase": "Gwellhe selvanylyon",
+ "TaskOptimizeDatabaseDescription": "Y hwra kesstrotha ha berrhe efander rydh. Martesen y hwra gwellhe gwryth mar kwre'ta an oberen ma wosa ty dhe arhwilas an lyverva, po neb chanj aral neb a brof chanjyansow selvanylyon.",
+ "TaskAudioNormalizationDescription": "Y hwra arhwilas restrennow rag manylyon normalheans klewans.",
+ "TaskRefreshLibraryDescription": "Y hwra arhwilas dha lyverva media rag restrennow nowydh ha disegha metamanylyon.",
+ "TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari",
+ "TaskKeyframeExtractor": "Estennell Framalhwedh",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.",
+ "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index b66818ddc..747652538 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
"TaskAudioNormalization": "Lyd Normalisering",
"TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes"
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes",
+ "TaskDownloadMissingLyrics": "Last ned manglende tekster",
+ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster"
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 016c5b163..662e2acbc 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -45,6 +45,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+ private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1);
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
@@ -1851,13 +1852,12 @@ public class DynamicHlsController : BaseJellyfinApiController
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
// Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
- if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion < _minFFmpegX265BframeInFmp4)
{
args += " -bf 0";
}
- // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
-
// video processing filters.
var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 22f58ad70..24cd141dc 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -67,6 +67,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegWorkingVtHwSurface = new Version(7, 0, 1);
private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0);
private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1);
+ private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1);
+ private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1);
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
@@ -296,14 +298,12 @@ namespace MediaBrowser.Controller.MediaEncoding
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)))
+ || !_mediaEncoder.SupportsFilter("tonemapx"))
{
return false;
}
- return state.VideoStream.VideoRange == VideoRange.HDR
- && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
+ return state.VideoStream.VideoRange == VideoRange.HDR;
}
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -349,7 +349,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& GetVideoColorBitDepth(state) == 10;
}
- private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ private bool IsIntelVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
|| !options.EnableVppTonemapping
@@ -358,7 +358,14 @@ namespace MediaBrowser.Controller.MediaEncoding
return false;
}
- // Native VPP tonemapping may come to QSV in the future.
+ // prefer 'tonemap_vaapi' over 'vpp_qsv' on Linux for supporting Gen9/KBLx.
+ // 'vpp_qsv' requires VPL, which is only supported on Gen12/TGLx and newer.
+ if (OperatingSystem.IsWindows()
+ && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion < _minFFmpegQsvVppTonemapOption)
+ {
+ return false;
+ }
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
@@ -879,17 +886,23 @@ namespace MediaBrowser.Controller.MediaEncoding
renderNodePath);
}
- private string GetQsvDeviceArgs(string alias)
+ private string GetQsvDeviceArgs(string renderNodePath, string alias)
{
var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias);
if (OperatingSystem.IsLinux())
{
// derive qsv from vaapi device
- return GetVaapiDeviceArgs(null, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias;
+ return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias;
}
if (OperatingSystem.IsWindows())
{
+ // on Windows, the deviceIndex is an int
+ if (int.TryParse(renderNodePath, NumberStyles.Integer, CultureInfo.InvariantCulture, out int deviceIndex))
+ {
+ return GetD3d11vaDeviceArgs(deviceIndex, string.Empty, D3d11vaAlias) + arg + "@" + D3d11vaAlias;
+ }
+
// derive qsv from d3d11va device
return GetD3d11vaDeviceArgs(0, "0x8086", D3d11vaAlias) + arg + "@" + D3d11vaAlias;
}
@@ -1049,7 +1062,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- args.Append(GetQsvDeviceArgs(QsvAlias));
+ args.Append(GetQsvDeviceArgs(options.QsvDevice, QsvAlias));
var filterDevArgs = GetFilterHwDeviceArgs(QsvAlias);
// child device used by qsv.
if (_mediaEncoder.SupportsHwaccel("vaapi") || _mediaEncoder.SupportsHwaccel("d3d11va"))
@@ -1484,7 +1497,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- // TODO: Perhaps also use original_size=1920x800 ??
return string.Format(
CultureInfo.InvariantCulture,
"subtitles=f='{0}'{1}{2}{3}{4}{5}",
@@ -1506,7 +1518,6 @@ namespace MediaBrowser.Controller.MediaEncoding
alphaParam,
sub2videoParam,
fontParam,
- // fallbackFontParam,
setPtsParam);
}
@@ -1659,7 +1670,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
&& IsVaapiSupported(state)
&& IsOpenclFullSupported()
- && !IsVaapiVppTonemapAvailable(state, encodingOptions)
+ && !IsIntelVppTonemapAvailable(state, encodingOptions)
&& IsHwTonemapAvailable(state, encodingOptions);
enableWaFori915Hang = isIntelDecoder && doOclTonemap;
@@ -1780,12 +1791,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += " -preset veryfast";
}
-
- // Only h264_qsv has look_ahead option
- if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- {
- param += " -look_ahead 0";
- }
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
@@ -2072,7 +2077,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
{
- param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none";
+ param += " -x264opts:0 subme=0:me_range=16:rc_lookahead=10:me=hex:open_gop=0";
}
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
@@ -2080,8 +2085,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// libx265 only accept level option in -x265-params.
// level option may cause libx265 to fail.
// libx265 cannot adjust the given level, just throw an error.
- // TODO: set fine tuned params.
- param += " -x265-params:0 no-info=1";
+ param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
}
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
@@ -3238,14 +3242,18 @@ namespace MediaBrowser.Controller.MediaEncoding
doubleRateDeint ? "1" : "0");
}
- public static string GetHwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options, string hwDeintSuffix)
+ public string GetHwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options, string hwDeintSuffix)
{
var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30;
if (hwDeintSuffix.Contains("cuda", StringComparison.OrdinalIgnoreCase))
{
+ var useBwdif = string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.SupportsFilter("bwdif_cuda");
+
return string.Format(
CultureInfo.InvariantCulture,
- "yadif_cuda={0}:-1:0",
+ "{0}_cuda={1}:-1:0",
+ useBwdif ? "bwdif" : "yadif",
doubleRateDeint ? "1" : "0");
}
@@ -3285,14 +3293,31 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
{
- args = "procamp_vaapi=b={1}:c={2},tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
+ var doVaVppProcamp = false;
+ var procampParams = string.Empty;
+ if (options.VppTonemappingBrightness != 0
+ && options.VppTonemappingBrightness >= -100
+ && options.VppTonemappingBrightness <= 100)
+ {
+ procampParams += $"=b={options.VppTonemappingBrightness}";
+ doVaVppProcamp = true;
+ }
+
+ if (options.VppTonemappingContrast > 1
+ && options.VppTonemappingContrast <= 10)
+ {
+ procampParams += doVaVppProcamp ? ":" : "=";
+ procampParams += $"c={options.VppTonemappingContrast}";
+ doVaVppProcamp = true;
+ }
+
+ args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
return string.Format(
CultureInfo.InvariantCulture,
args,
- videoFormat ?? "nv12",
- options.VppTonemappingBrightness,
- options.VppTonemappingContrast);
+ doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
+ videoFormat ?? "nv12");
}
else
{
@@ -3378,15 +3403,7 @@ namespace MediaBrowser.Controller.MediaEncoding
algorithm = "clip";
}
- tonemapArg = ":tonemapping=" + algorithm;
-
- if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase)
- || string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase))
- {
- tonemapArg += ":tonemapping_mode=" + mode;
- }
-
- tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
+ tonemapArg = ":tonemapping=" + algorithm + ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase))
@@ -3444,6 +3461,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doToneMap = IsSwTonemapAvailable(state, options);
+ var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI;
var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -3481,11 +3499,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw scale
mainFilters.Add(swScaleFilter);
- // sw tonemap <= TODO: finish dovi tone mapping
-
+ // sw tonemap
if (doToneMap)
{
- var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={outFormat}";
+ // tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
+ var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
+
+ var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
if (options.TonemappingParam != 0)
{
@@ -4021,7 +4041,9 @@ 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 doOclTonemap = IsHwTonemapAvailable(state, options);
+ var doVppTonemap = IsIntelVppTonemapAvailable(state, options);
+ var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options);
+ var doTonemap = doVppTonemap || doOclTonemap;
var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -4040,7 +4062,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/* Make main filters for video stream */
var mainFilters = new List<string>();
- mainFilters.Add(GetOverwriteColorPropertiesParam(state, doOclTonemap));
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, doTonemap));
if (isSwDecoder)
{
@@ -4068,9 +4090,33 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (isD3d11vaDecoder || isQsvDecoder)
{
+ var doVppProcamp = false;
+ var procampParams = string.Empty;
+ if (doVppTonemap)
+ {
+ if (options.VppTonemappingBrightness != 0
+ && options.VppTonemappingBrightness >= -100
+ && options.VppTonemappingBrightness <= 100)
+ {
+ procampParams += $":brightness={options.VppTonemappingBrightness}";
+ doVppProcamp = true;
+ }
+
+ if (options.VppTonemappingContrast > 1
+ && options.VppTonemappingContrast <= 10)
+ {
+ procampParams += $":contrast={options.VppTonemappingContrast}";
+ doVppProcamp = true;
+ }
+
+ procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
+ }
+
var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12";
+ outFormat = (doVppTonemap && doVppProcamp) ? "p010" : outFormat;
+
var swapOutputWandH = doVppTranspose && swapWAndH;
- var hwScalePrefix = doVppTranspose ? "vpp" : "scale";
+ var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale";
var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
@@ -4078,6 +4124,11 @@ namespace MediaBrowser.Controller.MediaEncoding
hwScaleFilter += $":transpose={tranposeDir}";
}
+ if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap)
+ {
+ hwScaleFilter += doVppProcamp ? procampParams : ":tonemap=1";
+ }
+
if (isD3d11vaDecoder)
{
if (!string.IsNullOrEmpty(hwScaleFilter) || doDeintH2645)
@@ -4095,8 +4146,20 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(deintFilter);
}
- // hw transpose & scale
+ // hw transpose & scale & tonemap(w/o procamp)
mainFilters.Add(hwScaleFilter);
+
+ // hw tonemap(w/ procamp)
+ if (doVppTonemap && doVppProcamp)
+ {
+ mainFilters.Add("vpp_qsv=tonemap=1:format=nv12:async_depth=2");
+ }
+
+ // force bt709 just in case vpp tonemap is not triggered or using MSDK instead of VPL.
+ if (doVppTonemap)
+ {
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, false));
+ }
}
if (doOclTonemap && isHwDecoder)
@@ -4229,7 +4292,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 doVaVppTonemap = IsVaapiVppTonemapAvailable(state, options);
+ var doVaVppTonemap = IsIntelVppTonemapAvailable(state, options);
var doOclTonemap = !doVaVppTonemap && IsHwTonemapAvailable(state, options);
var doTonemap = doVaVppTonemap || doOclTonemap;
var doDeintH2645 = doDeintH264 || doDeintHevc;
@@ -4540,7 +4603,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 doVaVppTonemap = isVaapiDecoder && IsVaapiVppTonemapAvailable(state, options);
+ var doVaVppTonemap = isVaapiDecoder && IsIntelVppTonemapAvailable(state, options);
var doOclTonemap = !doVaVppTonemap && IsHwTonemapAvailable(state, options);
var doTonemap = doVaVppTonemap || doOclTonemap;
var doDeintH2645 = doDeintH264 || doDeintHevc;
@@ -4812,8 +4875,34 @@ namespace MediaBrowser.Controller.MediaEncoding
if (doVkTranspose || doVkTonemap || hasSubs)
{
// map from vaapi to vulkan/drm via interop (Polaris/gfx8+).
- mainFilters.Add("hwmap=derive_device=vulkan");
- mainFilters.Add("format=vulkan");
+ if (_mediaEncoder.EncoderVersion >= _minFFmpegAlteredVaVkInterop)
+ {
+ if (doVkTranspose || !_mediaEncoder.IsVaapiDeviceSupportVulkanDrmModifier)
+ {
+ // disable the indirect va-drm-vk mapping since it's no longer reliable.
+ mainFilters.Add("hwmap=derive_device=drm");
+ mainFilters.Add("format=drm_prime");
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
+
+ // workaround for libplacebo using the imported vulkan frame on gfx8.
+ if (!_mediaEncoder.IsVaapiDeviceSupportVulkanDrmModifier)
+ {
+ mainFilters.Add("scale_vulkan");
+ }
+ }
+ else if (doVkTonemap || hasSubs)
+ {
+ // non ad-hoc libplacebo also accepts drm_prime direct input.
+ mainFilters.Add("hwmap=derive_device=drm");
+ mainFilters.Add("format=drm_prime");
+ }
+ }
+ else // legacy va-vk mapping that works only in jellyfin-ffmpeg6
+ {
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
+ }
}
else
{
@@ -4848,6 +4937,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
mainFilters.Add(libplaceboFilter);
+ mainFilters.Add("format=vulkan");
}
if (doVkTonemap && !hasSubs)
@@ -5144,13 +5234,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null, null);
}
+ // ReSharper disable once InconsistentNaming
var isMacOS = OperatingSystem.IsMacOS();
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
+ var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
var isVtFullSupported = isMacOS && IsVideoToolboxFullSupported();
// legacy videotoolbox pipeline (disable hw filters)
- if (!isVtEncoder
+ if (!(isVtEncoder || isVtDecoder)
|| !isVtFullSupported
|| !_mediaEncoder.SupportsFilter("alphasrc"))
{
@@ -5170,12 +5262,6 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
- if (!isVtEncoder)
- {
- // should not happen.
- return (null, null, null);
- }
-
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
var reqW = state.BaseRequest.Width;
@@ -5290,6 +5376,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (usingHwSurface)
{
+ if (!isVtEncoder)
+ {
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
+
return (mainFilters, subFilters, overlayFilters);
}
@@ -5303,6 +5395,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// this will pass-through automatically if in/out format matches.
mainFilters.Insert(0, "hwupload");
mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
+
+ if (!isVtEncoder)
+ {
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
}
return (mainFilters, subFilters, overlayFilters);
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index e36106e52..c767b4a51 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -66,6 +66,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
/// </summary>
+ /// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value>
+ bool IsVaapiDeviceSupportVulkanDrmModifier { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the configured Vaapi device supports vulkan drm interop via dma-buf.
+ /// </summary>
/// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value>
bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 6c43be3ab..2b6ed8fa0 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -110,6 +110,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// cuda
"scale_cuda",
"yadif_cuda",
+ "bwdif_cuda",
"tonemap_cuda",
"overlay_cuda",
"transpose_cuda",
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index c5f500e76..2daeac734 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
- var files = string.Join("|", inputFiles.Select(NormalizePath));
+ var files = string.Join('|', inputFiles.Select(NormalizePath));
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index d9fe0594f..764230feb 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
@@ -79,8 +80,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
private bool _isVaapiDeviceInteli965 = false;
+ private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
+ private static string[] _vulkanImageDrmFmtModifierExts =
+ {
+ "VK_EXT_image_drm_format_modifier",
+ };
+
private static string[] _vulkanExternalMemoryDmaBufExts =
{
"VK_KHR_external_memory_fd",
@@ -141,6 +148,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
/// <inheritdoc />
+ public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
+
+ /// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
@@ -219,6 +229,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
+ _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanImageDrmFmtModifierExts);
_isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanExternalMemoryDmaBufExts);
if (_isVaapiDeviceAmd)
@@ -234,6 +245,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
}
+ if (_isVaapiDeviceSupportVulkanDrmModifier)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.VaapiDevice);
+ }
+
if (_isVaapiDeviceSupportVulkanDrmInterop)
{
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
@@ -665,18 +681,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
}
- // Use SW tonemap on HDR10/HLG video stream only when the zscale or tonemapx filter is available.
+ // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
+ // Only enable Dolby Vision tonemap when tonemapx is available
var enableHdrExtraction = false;
- if (string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+ if (videoStream?.VideoRange == VideoRange.HDR)
{
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"))
+ else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
{
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");
@@ -719,8 +735,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
using (var processWrapper = new ProcessWrapper(process, this))
{
- bool ranToCompletion;
-
using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -734,22 +748,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
- ranToCompletion = true;
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
process.Kill(true);
- ranToCompletion = false;
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction timed out for {0} after {1}ms", inputPath, timeoutMs), ex);
}
}
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
var file = _fileSystem.GetFileInfo(tempExtractPath);
- if (exitCode == -1 || !file.Exists || file.Length == 0)
+ if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
{
- _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath);
-
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath));
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index fd55db4ba..a79d801fb 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -54,12 +54,23 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
break;
}
-
- _logger.LogError(
- "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
- subtitleFormat.ErrorCount,
- fileExtension,
- subtitleFormat.Name);
+ else if (subtitleFormat.TryGetErrors(out var errors))
+ {
+ _logger.LogError(
+ "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser, errors: {Errors}",
+ subtitleFormat.ErrorCount,
+ fileExtension,
+ subtitleFormat.Name,
+ errors);
+ }
+ else
+ {
+ _logger.LogError(
+ "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
+ subtitleFormat.ErrorCount,
+ fileExtension,
+ subtitleFormat.Name);
+ }
}
if (subtitle.Paragraphs.Count == 0)
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs
new file mode 100644
index 000000000..88c2bf3db
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics.CodeAnalysis;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
+
+namespace MediaBrowser.MediaEncoding.Subtitles;
+
+internal static class SubtitleFormatExtensions
+{
+ /// <summary>
+ /// Will try to find errors if supported by provider.
+ /// </summary>
+ /// <param name="format">The subtitle format.</param>
+ /// <param name="errors">The out errors value.</param>
+ /// <returns>True if errors are available for given format.</returns>
+ public static bool TryGetErrors(this SubtitleFormat format, [NotNullWhen(true)] out string? errors)
+ {
+ errors = format switch
+ {
+ SubStationAlpha ssa => ssa.Errors,
+ AdvancedSubStationAlpha assa => assa.Errors,
+ SubRip subRip => subRip.Errors,
+ MicroDvd microDvd => microDvd.Errors,
+ DCinemaSmpte2007 smpte2007 => smpte2007.Errors,
+ DCinemaSmpte2010 smpte2010 => smpte2010.Errors,
+ _ => null,
+ };
+
+ return !string.IsNullOrWhiteSpace(errors);
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 9a192f584..4c5213d4e 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -26,6 +26,7 @@ public class EncodingOptions
// This is a DRM device that is almost guaranteed to be there on every intel platform,
// plus it's the default one in ffmpeg if you don't specify anything
VaapiDevice = "/dev/dri/renderD128";
+ QsvDevice = string.Empty;
EnableTonemapping = false;
EnableVppTonemapping = false;
EnableVideoToolboxTonemapping = false;
@@ -137,6 +138,11 @@ public class EncodingOptions
public string VaapiDevice { get; set; }
/// <summary>
+ /// Gets or sets the QSV device.
+ /// </summary>
+ public string QsvDevice { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether tonemapping is enabled.
/// </summary>
public bool EnableTonemapping { get; set; }
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 90035f18f..5d65b0f9b 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -26,7 +27,7 @@ namespace MediaBrowser.Model.Net
/// <summary>
/// Any extension in this list is considered a video file.
/// </summary>
- private static readonly HashSet<string> _videoFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly FrozenSet<string> _videoFileExtensions = new[]
{
".3gp",
".asf",
@@ -57,90 +58,90 @@ namespace MediaBrowser.Model.Net
".webm",
".wmv",
".wtv",
- };
+ }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Used for extensions not in <see cref="Model.MimeTypes"/> or to override them.
/// </summary>
- private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly FrozenDictionary<string, string> _mimeTypeLookup = new KeyValuePair<string, string>[]
{
// Type application
- { ".azw3", "application/vnd.amazon.ebook" },
- { ".cb7", "application/x-cb7" },
- { ".cba", "application/x-cba" },
- { ".cbr", "application/vnd.comicbook-rar" },
- { ".cbt", "application/x-cbt" },
- { ".cbz", "application/vnd.comicbook+zip" },
+ new(".azw3", "application/vnd.amazon.ebook"),
+ new(".cb7", "application/x-cb7"),
+ new(".cba", "application/x-cba"),
+ new(".cbr", "application/vnd.comicbook-rar"),
+ new(".cbt", "application/x-cbt"),
+ new(".cbz", "application/vnd.comicbook+zip"),
// Type image
- { ".tbn", "image/jpeg" },
+ new(".tbn", "image/jpeg"),
// Type text
- { ".ass", "text/x-ssa" },
- { ".ssa", "text/x-ssa" },
- { ".edl", "text/plain" },
- { ".html", "text/html; charset=UTF-8" },
- { ".htm", "text/html; charset=UTF-8" },
+ new(".ass", "text/x-ssa"),
+ new(".ssa", "text/x-ssa"),
+ new(".edl", "text/plain"),
+ new(".html", "text/html; charset=UTF-8"),
+ new(".htm", "text/html; charset=UTF-8"),
// Type video
- { ".mpegts", "video/mp2t" },
+ new(".mpegts", "video/mp2t"),
// Type audio
- { ".aac", "audio/aac" },
- { ".ac3", "audio/ac3" },
- { ".ape", "audio/x-ape" },
- { ".dsf", "audio/dsf" },
- { ".dsp", "audio/dsp" },
- { ".flac", "audio/flac" },
- { ".m4b", "audio/mp4" },
- { ".mp3", "audio/mpeg" },
- { ".vorbis", "audio/vorbis" },
- { ".webma", "audio/webm" },
- { ".wv", "audio/x-wavpack" },
- { ".xsp", "audio/xsp" },
- };
-
- private static readonly Dictionary<string, string> _extensionLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ new(".aac", "audio/aac"),
+ new(".ac3", "audio/ac3"),
+ new(".ape", "audio/x-ape"),
+ new(".dsf", "audio/dsf"),
+ new(".dsp", "audio/dsp"),
+ new(".flac", "audio/flac"),
+ new(".m4b", "audio/mp4"),
+ new(".mp3", "audio/mpeg"),
+ new(".vorbis", "audio/vorbis"),
+ new(".webma", "audio/webm"),
+ new(".wv", "audio/x-wavpack"),
+ new(".xsp", "audio/xsp"),
+ }.ToFrozenDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
+
+ private static readonly FrozenDictionary<string, string> _extensionLookup = new KeyValuePair<string, string>[]
{
// Type application
- { "application/vnd.comicbook-rar", ".cbr" },
- { "application/vnd.comicbook+zip", ".cbz" },
- { "application/x-cb7", ".cb7" },
- { "application/x-cba", ".cba" },
- { "application/x-cbr", ".cbr" },
- { "application/x-cbt", ".cbt" },
- { "application/x-cbz", ".cbz" },
- { "application/x-javascript", ".js" },
- { "application/xml", ".xml" },
- { "application/x-mpegURL", ".m3u8" },
+ new("application/vnd.comicbook-rar", ".cbr"),
+ new("application/vnd.comicbook+zip", ".cbz"),
+ new("application/x-cb7", ".cb7"),
+ new("application/x-cba", ".cba"),
+ new("application/x-cbr", ".cbr"),
+ new("application/x-cbt", ".cbt"),
+ new("application/x-cbz", ".cbz"),
+ new("application/x-javascript", ".js"),
+ new("application/xml", ".xml"),
+ new("application/x-mpegURL", ".m3u8"),
// Type audio
- { "audio/aac", ".aac" },
- { "audio/ac3", ".ac3" },
- { "audio/dsf", ".dsf" },
- { "audio/dsp", ".dsp" },
- { "audio/flac", ".flac" },
- { "audio/m4b", ".m4b" },
- { "audio/vorbis", ".vorbis" },
- { "audio/x-ape", ".ape" },
- { "audio/xsp", ".xsp" },
- { "audio/x-wavpack", ".wv" },
+ new("audio/aac", ".aac"),
+ new("audio/ac3", ".ac3"),
+ new("audio/dsf", ".dsf"),
+ new("audio/dsp", ".dsp"),
+ new("audio/flac", ".flac"),
+ new("audio/m4b", ".m4b"),
+ new("audio/vorbis", ".vorbis"),
+ new("audio/x-ape", ".ape"),
+ new("audio/xsp", ".xsp"),
+ new("audio/x-wavpack", ".wv"),
// Type image
- { "image/jpeg", ".jpg" },
- { "image/tiff", ".tiff" },
- { "image/x-png", ".png" },
- { "image/x-icon", ".ico" },
+ new("image/jpeg", ".jpg"),
+ new("image/tiff", ".tiff"),
+ new("image/x-png", ".png"),
+ new("image/x-icon", ".ico"),
// Type text
- { "text/plain", ".txt" },
- { "text/rtf", ".rtf" },
- { "text/x-ssa", ".ssa" },
+ new("text/plain", ".txt"),
+ new("text/rtf", ".rtf"),
+ new("text/x-ssa", ".ssa"),
// Type video
- { "video/vnd.mpeg.dash.mpd", ".mpd" },
- { "video/x-matroska", ".mkv" },
- };
+ new("video/vnd.mpeg.dash.mpd", ".mpd"),
+ new("video/x-matroska", ".mkv"),
+ }.ToFrozenDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
index 67b26e457..fffdf4887 100644
--- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
+++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Extensions;
@@ -20,7 +19,6 @@ public class LrcLyricParser : ILyricParser
private readonly LyricParser _lrcLyricParser;
private static readonly string[] _supportedMediaTypes = [".lrc", ".elrc"];
- private static readonly string[] _acceptedTimeFormats = ["HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss"];
/// <summary>
/// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
@@ -59,37 +57,7 @@ public class LrcLyricParser : ILyricParser
return null;
}
- List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.Where(x => x.TimeTags.Count > 0).OrderBy(x => x.TimeTags.First().Value).ToList();
-
- // Parse metadata rows
- var metaDataRows = lyricData.Lyrics
- .Where(x => x.TimeTags.Count == 0)
- .Where(x => x.Text.StartsWith('[') && x.Text.EndsWith(']'))
- .Select(x => x.Text)
- .ToList();
-
- var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- foreach (string metaDataRow in metaDataRows)
- {
- var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase);
- if (index == -1)
- {
- continue;
- }
-
- // Remove square bracket before field name, and after field value
- // Example 1: [au: 1hitsong]
- // Example 2: [ar: Calabrese]
- var metaDataFieldName = GetMetadataFieldName(metaDataRow, index);
- var metaDataFieldValue = GetMetadataValue(metaDataRow, index);
-
- if (string.IsNullOrEmpty(metaDataFieldName) || string.IsNullOrEmpty(metaDataFieldValue))
- {
- continue;
- }
-
- fileMetaData[metaDataFieldName] = metaDataFieldValue;
- }
+ List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.OrderBy(x => x.StartTime).ToList();
if (sortedLyricData.Count == 0)
{
@@ -100,99 +68,10 @@ public class LrcLyricParser : ILyricParser
for (int i = 0; i < sortedLyricData.Count; i++)
{
- var timeData = sortedLyricData[i].TimeTags.First().Value;
- if (timeData is null)
- {
- continue;
- }
-
- long ticks = TimeSpan.FromMilliseconds(timeData.Value).Ticks;
+ long ticks = TimeSpan.FromMilliseconds(sortedLyricData[i].StartTime).Ticks;
lyricList.Add(new LyricLine(sortedLyricData[i].Text.Trim(), ticks));
}
- if (fileMetaData.Count != 0)
- {
- // Map metaData values from LRC file to LyricMetadata properties
- LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
-
- return new LyricDto { Metadata = lyricMetadata, Lyrics = lyricList };
- }
-
return new LyricDto { Lyrics = lyricList };
}
-
- /// <summary>
- /// Converts metadata from an LRC file to LyricMetadata properties.
- /// </summary>
- /// <param name="metaData">The metadata from the LRC file.</param>
- /// <returns>A lyricMetadata object with mapped property data.</returns>
- private static LyricMetadata MapMetadataValues(Dictionary<string, string> metaData)
- {
- LyricMetadata lyricMetadata = new();
-
- if (metaData.TryGetValue("ar", out var artist) && !string.IsNullOrEmpty(artist))
- {
- lyricMetadata.Artist = artist;
- }
-
- if (metaData.TryGetValue("al", out var album) && !string.IsNullOrEmpty(album))
- {
- lyricMetadata.Album = album;
- }
-
- if (metaData.TryGetValue("ti", out var title) && !string.IsNullOrEmpty(title))
- {
- lyricMetadata.Title = title;
- }
-
- if (metaData.TryGetValue("au", out var author) && !string.IsNullOrEmpty(author))
- {
- lyricMetadata.Author = author;
- }
-
- if (metaData.TryGetValue("length", out var length) && !string.IsNullOrEmpty(length))
- {
- if (DateTime.TryParseExact(length, _acceptedTimeFormats, null, DateTimeStyles.None, out var value))
- {
- lyricMetadata.Length = value.TimeOfDay.Ticks;
- }
- }
-
- if (metaData.TryGetValue("by", out var by) && !string.IsNullOrEmpty(by))
- {
- lyricMetadata.By = by;
- }
-
- if (metaData.TryGetValue("offset", out var offset) && !string.IsNullOrEmpty(offset))
- {
- if (int.TryParse(offset, out var value))
- {
- lyricMetadata.Offset = TimeSpan.FromMilliseconds(value).Ticks;
- }
- }
-
- if (metaData.TryGetValue("re", out var creator) && !string.IsNullOrEmpty(creator))
- {
- lyricMetadata.Creator = creator;
- }
-
- if (metaData.TryGetValue("ve", out var version) && !string.IsNullOrEmpty(version))
- {
- lyricMetadata.Version = version;
- }
-
- return lyricMetadata;
- }
-
- private static string GetMetadataFieldName(string metaDataRow, int index)
- {
- var metadataFieldName = metaDataRow.AsSpan(1, index - 1).Trim();
- return metadataFieldName.IsEmpty ? string.Empty : metadataFieldName.ToString();
- }
-
- private static string GetMetadataValue(string metaDataRow, int index)
- {
- var metadataValue = metaDataRow.AsSpan(index + 1, metaDataRow.Length - index - 2).Trim();
- return metadataValue.IsEmpty ? string.Empty : metadataValue.ToString();
- }
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs
new file mode 100644
index 000000000..dd971fa87
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs
@@ -0,0 +1,96 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.LiveTv;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public LiveTvControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task AddTunerHost_Unauthorized_ReturnsUnauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_Valid_ReturnsCorrectResponse()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+ var responseBody = await response.Content.ReadFromJsonAsync<TunerHostInfo>();
+ Assert.NotNull(responseBody);
+ Assert.Equal(body.Type, responseBody.Type);
+ Assert.Equal(body.Url, responseBody.Url);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_InvalidType_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "invalid",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_InvalidUrl_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "thisgoesnowhere"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs
new file mode 100644
index 000000000..547bfcc0f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs
@@ -0,0 +1,45 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Plugins;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public PluginsControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetPlugins_Unauthorized_ReturnsUnauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/Plugins");
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetPlugins_Authorized_ReturnsCorrectResponse()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var response = await client.GetAsync("/Plugins");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+ _ = await response.Content.ReadFromJsonAsync<PluginInfo[]>(JsonDefaults.Options);
+ }
+}
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 a5296d8c9..8228c0df7 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -18,6 +18,9 @@
</ItemGroup>
<ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
<!-- Don't run tests in parallel -->
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
diff --git a/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8 b/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8
new file mode 100644
index 000000000..7f60f38a6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8
@@ -0,0 +1 @@
+C:\Music