Compare commits

..

69 Commits

Author SHA1 Message Date
gnattu
44d8d53fb1 report 4.4 for now 2026-07-19 19:54:41 +08:00
gnattu
2f9bae965f fix unit test version 2026-07-19 19:40:35 +08:00
gnattu
5f5442cb12 Remove libpostproc check for ffmpeg version validation
This is a deprecated lib and is removed in ffmpeg 8.1. Enforcing this would make recent git build for ffmpeg to fail this check.
2026-07-19 19:39:01 +08:00
CHO-HSUN-TE
3321a0cf09 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/
2026-07-18 12:21:45 +00:00
Bond-009
dd0b273b26 Fix Swagger UI auth docs (#12990) (#16910)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2026-07-17 23:48:04 +02:00
Marc Brooks
cab108a839 Prevent ffmpeg from hanging extracting subtitles (#17297)
* Prevent ffmpeg from hanging extracting subtitles
Add `RunSubtitleExtractionProcess` to unify the external
_ffmpeg_ process handling and error management.
Add a `-nostdin` flag that prevents _ffmpeg_ from reading from
_stdin_ and blocking on an inherited stdin handle (e.g. when
Jellyfin runs as a service under NSSM), which otherwise hangs
subtitle extraction forever when _ffmpeg_ blocks on any
keyboard-interaction read until the timeout (30 minutes).
Close the redirected _stdin_ to ensure immediage EOF.
Drain the _stderr_ to a string and log it, to ensure we don't block
the _ffmpeg_ process on errors that exceed the pipe length.
Pass `-y` to _ffmpeg_ to ensure it overwrites any existing output file
without prompting for confirmation.

* Address review comments
Make sure we always drain stderr.
Make sure the timeout also honors the cancellationToken.
Make sure when we get cancelled we don't log it as a ffmpeg error.
2026-07-17 23:23:57 +02:00
Rant423
3d83e67a52 Show production companies under TV Shows' Studios (#17246)
* show production companies instead of networks

* keep both production companies and networks

* fix whitespace

* fix nullable type

* networks first, then production companies
2026-07-17 22:45:54 +02:00
Bond-009
b809d964b3 Merge pull request #17288 from nyanmisaka/ffmpeg-log-utf8
Fix potential garbled text in FFmpeg logs on Windows
2026-07-17 22:28:49 +02:00
Bond-009
96727072c8 Merge pull request #17327 from Shadowghost/remove-playbackpositionticks-mediasourceinfo
Remove PlaybackPositionTicks from MediaSourceInfo
2026-07-17 22:21:21 +02:00
Bond-009
9ec0d7c63b Merge pull request #17334 from nyanmisaka/fix-cuda-hwupload
Fix format negotiation in hybrid SW decode and CUDA tonemap pipeline
2026-07-17 22:15:49 +02:00
Bond-009
bfe0a47b32 Merge pull request #17151 from theguymadmax/fix-indentify-image
Fix Identify returning wrong images
2026-07-17 22:13:47 +02:00
Bond-009
9b5f830462 Merge pull request #17326 from theguymadmax/update-series-name
Update season and episode SeriesName when renaming a series
2026-07-17 22:06:26 +02:00
Bond-009
42f9ed76c0 Merge pull request #17280 from Shadowghost/remove-image-override-hack
Remove episode image override hack
2026-07-17 22:06:07 +02:00
Bond-009
69faa6c583 Merge pull request #17191 from IDisposable/fix/handler-path-traversal
Fix path transversal exposure in Plugins
2026-07-17 21:52:22 +02:00
Bond-009
a96dc8bd9b Merge pull request #17354 from jellyfin/renovate/actions-setup-dotnet-6.x
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2026-07-17 13:56:55 +02:00
Bond-009
d146c347b4 Merge pull request #17359 from jellyfin/renovate/ci-deps 2026-07-17 13:54:14 +02:00
Bond-009
f2dea2a3fd Merge pull request #17348 from theguymadmax/revert-includeItemTypes-in-collectionType 2026-07-17 13:53:58 +02:00
Bond-009
4d1dd42420 Merge pull request #17343 from mbastian77/docs/channels-xml-docs 2026-07-17 13:53:27 +02:00
Bond-009
8eb0ba60c1 Merge pull request #17344 from mbastian77/docs/session-model-xml-docs 2026-07-17 13:52:59 +02:00
Bond-009
2bf15d95df Merge pull request #17340 from mbastian77/docs/dlna-model-xml-docs 2026-07-17 13:52:31 +02:00
Bond-009
be2db99586 Merge pull request #17339 from mbastian77/docs/providers-lookup-info-xml-docs 2026-07-17 13:51:59 +02:00
Bond-009
37fe9df27c Merge pull request #17338 from mbastian77/docs/deviceid-xml-docs 2026-07-17 13:51:29 +02:00
Fabián Sanhueza
8759ad4d49 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_419/
2026-07-17 11:49:50 +00:00
Bond-009
051ff4cf6d Merge pull request #17337 from Shadowghost/limit-similar-items 2026-07-17 13:49:46 +02:00
theguymadmax
fa4626c080 Revert setting default BaseItemKind for CollectionType 2026-07-16 18:15:54 -04:00
renovate[bot]
4cc577a72d Update github/codeql-action action to v4.37.1 2026-07-16 18:47:00 +00:00
renovate[bot]
2b57a5725c Update actions/setup-dotnet action to v6 2026-07-16 07:02:47 +00:00
mbastian77
5cbafa566d Add XML docs to small session model types and remove CS1591 suppressions 2026-07-15 16:03:56 +02:00
mbastian77
0d3cf0169e Add XML docs to small channel types and remove CS1591 suppressions 2026-07-15 16:02:12 +02:00
Bond-009
f8771b52ec Merge pull request #17331 from jellyfin/renovate/dotnet-monorepo
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Format / format-check (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2026-07-15 12:49:34 +02:00
Bond-009
e3d610c3c5 Merge pull request #17274 from theguymadmax/fix-max-login-attempts 2026-07-15 12:42:09 +02:00
Bond-009
486ffaa6dd Merge pull request #17248 from theguymadmax/add-novel 2026-07-15 12:40:17 +02:00
mbastian77
d96d1f1119 Add XML docs to small DLNA model types and remove CS1591 suppressions 2026-07-15 12:11:11 +02:00
mbastian77
30f28456de Add XML docs to lookup info types and remove CS1591 suppressions 2026-07-15 12:08:08 +02:00
mbastian77
f8aad322cc Add XML docs to DeviceId and remove CS1591 suppression 2026-07-15 11:58:29 +02:00
Shadowghost
c6545a8b68 Limit similar items to user accessible libraries 2026-07-15 11:47:59 +02:00
Bond-009
0fe9c1ce91 Merge pull request #17330 from jellyfin/renovate/microsoft 2026-07-15 11:42:52 +02:00
Bond-009
b6882c86dc Merge pull request #17250 from TaterTechStudios/feature/books-series-name-metadata
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2026-07-15 07:57:57 +02:00
nyanmisaka
4503ad295c Fix format negotiation in hybrid SW decode and CUDA tonemap pipeline
The CUDA hwcontext in FFmpeg 8.1 has added support for 10bit
fully-planar formats, but few CUDA filters support them.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-07-15 13:54:45 +08:00
renovate[bot]
1a9bd2417a Update Microsoft 2026-07-14 20:11:54 +00:00
renovate[bot]
0d2f1b26b3 Update dependency dotnet-ef to v10.0.10 2026-07-14 20:11:43 +00:00
Bond-009
bc59e56ef5 Merge pull request #17266 from Shadowghost/fix-non-admin-additional-parts
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2026-07-14 15:36:38 +02:00
Shadowghost
b8bac71270 remove PlaybackPositionTicks from MediaSourceInfo 2026-07-14 10:05:32 +02:00
theguymadmax
6238448716 Update season and episode SeriesName when renaming a series 2026-07-13 23:08:06 -04:00
Bond-009
cfeaef6180 Merge pull request #17306 from theguymadmax/fix-strm-sub-protocol
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2026-07-13 20:14:20 +02:00
Bond-009
ebab369eac Merge pull request #17273 from theguymadmax/ratigs-numerical-score 2026-07-13 20:13:29 +02:00
DrummingBird1
0584a102ee Translated using Weblate (Hebrew)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2026-07-13 11:26:23 +00:00
theguymadmax
2aae53bc15 Apply review feedback 2026-07-12 11:54:56 -04:00
Bond-009
911044b415 Merge pull request #17287 from dkanada/audiobook-cover
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2026-07-12 17:39:34 +02:00
Bond-009
b77def6883 Merge pull request #17268 from theguymadmax/fix-greece-ratings 2026-07-12 17:36:01 +02:00
Bond-009
5c48935fa8 Merge pull request #17220 from jellyfin/renovate/ci-deps 2026-07-12 17:33:36 +02:00
theguymadmax
e2d9d592bc Fix incorrect protocol used for subtitle charset detection 2026-07-11 17:33:06 -04:00
dkanada
305160d2e4 enable audiobook image saving to local folder 2026-07-11 11:00:46 +09:00
renovate[bot]
933f7eea1b Update CI dependencies 2026-07-10 07:47:01 +00:00
nyanmisaka
631a314d24 Fix potential garbled text in FFmpeg logs on Windows
Explicitly set StandardErrorEncoding and StandardOutputEncoding to
Encoding.UTF8 when invoking the FFmpeg subprocess.

This prevents log encoding issues and character corruption on Windows
environments that default to non-UTF8 ANSI code pages.

This fixes garbled metadata and font names in the FFmpeg logs.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-07-10 14:46:27 +08:00
dkanada
ed06fd5139 support external images for audiobooks 2026-07-10 14:19:45 +09:00
Shadowghost
38813f7d42 Cleanup PreferEpisodeParentPoster) 2026-07-09 12:07:13 +02:00
Shadowghost
853922443f Remove episode image override hack 2026-07-09 11:54:18 +02:00
theguymadmax
8b826d981b Fix max login attempts 2026-07-08 23:07:57 -04:00
theguymadmax
8bf1710e07 Check numeric rating value after splitting country code 2026-07-08 20:09:49 -04:00
theguymadmax
5e42941d3b Fix Greece parental ratings 2026-07-08 12:09:38 -04:00
theguymadmax
a1255bda6a Use FrozenSet 2026-07-08 11:45:16 -04:00
Shadowghost
9a2fdb3573 Fix additional parts for non-admins 2026-07-08 13:43:30 +02:00
Jordan Rushing
f3fbe5575a Allow SeriesName to be editable from Item Metadata (books) 2026-07-06 15:28:49 -05:00
theguymadmax
860cb75d58 Add Novel job mapping to the Writing department 2026-07-06 11:27:24 -04:00
Marc Brooks
95cebffa87 Add tests
Also fixed a sibling directory that matches the prefix.
2026-06-28 16:26:35 -05:00
Marc Brooks
617ebf367f Fix path transversal exposure in Plugins
The request path is not validated to a valid path and could allow escaping the transcode path and downloading of any arbitrary file in GetHlsPlaylistLegacy .

GetHlsAudioSegmentLegacy and GetHlsVideoSegmentLegacy have the same issue, and are NOT behind an Authorize so they are publicly exploitable.

Added a ValidateTranscodePath that verifies that requested file paths start with the transcode path setting. Also ensure that all filename comparisons are OrdinalIgnoreCase because we might be running on a filesystem where filename-casing doesn't have to match. Switched from InvariantCulture because the underlying OS filename comparisons are always byte-wise (with case insensitivity here).

Fixed a similar issue in GetPluginImage
2026-06-26 12:05:54 -05:00
theguymadmax
c49f279a27 Fix Identify returning wrong images 2026-06-21 18:22:00 -04:00
chasus
9569b4550d Fix Swagger UI auth docs (#12990) 2026-05-24 00:04:10 +10:00
73 changed files with 810 additions and 388 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.9",
"version": "10.0.10",
"commands": [
"dotnet-ef"
]

View File

@@ -27,18 +27,18 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1

View File

@@ -17,7 +17,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
@@ -47,7 +47,7 @@ jobs:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true

View File

@@ -28,7 +28,7 @@ jobs:
repository: ${{ inputs.repository }}
- name: Configure .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true

View File

@@ -26,28 +26,28 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -74,10 +74,10 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="5.1.1" />
<PackageVersion Include="Svg.Skia" Version="3.7.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
<PackageVersion Include="System.Text.Json" Version="10.0.10" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />

View File

@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
@@ -10,6 +8,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Devices
{
/// <summary>
/// Provides the persistent unique identifier of this server installation.
/// </summary>
public class DeviceId
{
private readonly IApplicationPaths _appPaths;
@@ -18,12 +19,20 @@ namespace Emby.Server.Implementations.Devices
private string? _id;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceId"/> class.
/// </summary>
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DeviceId}"/> interface.</param>
public DeviceId(IApplicationPaths appPaths, ILogger<DeviceId> logger)
{
_appPaths = appPaths;
_logger = logger;
}
/// <summary>
/// Gets the device id, loading it from disk or generating and persisting a new one if none exists.
/// </summary>
public string Value => _id ??= GetDeviceId();
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");

View File

@@ -1385,38 +1385,22 @@ namespace Emby.Server.Implementations.Dto
}
}
if (options.PreferEpisodeParentPoster)
if (options.GetImageLimit(ImageType.Primary) > 0)
{
var episodeSeason = episode.Season;
var seasonPrimaryTag = episodeSeason is not null
? GetTagAndFillBlurhash(dto, episodeSeason, ImageType.Primary)
: null;
BaseItem? posterParent = null;
if (seasonPrimaryTag is not null)
{
dto.ParentPrimaryImageItemId = episodeSeason!.Id;
dto.ParentPrimaryImageTag = seasonPrimaryTag;
posterParent = episodeSeason;
}
else if (episodeSeries is not null && dto.SeriesPrimaryImageTag is not null)
{
dto.ParentPrimaryImageItemId = episodeSeries.Id;
dto.ParentPrimaryImageTag = dto.SeriesPrimaryImageTag;
posterParent = episodeSeries;
}
if (posterParent is not null)
{
if (dto.ImageTags is not null && dto.ImageTags.Remove(ImageType.Primary, out var ownPrimaryTag))
{
// Only drop the episode's own primary blurhash; keep the poster parent's.
dto.ImageBlurHashes?.GetValueOrDefault(ImageType.Primary)?.Remove(ownPrimaryTag);
}
dto.SeriesPrimaryImageTag = null;
dto.PrimaryImageAspectRatio = null;
AttachPrimaryImageAspectRatio(dto, posterParent);
}
}

View File

@@ -418,10 +418,10 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
/// Populates each source's own playback position for the user and, when the queried item is a
/// primary, moves the most recently played version to the front so that resuming without an
/// explicit source selection plays the version that was last watched. A directly queried
/// alternate version keeps its own source first.
/// When the queried item is a primary, moves the most recently played version to the front so
/// that resuming without an explicit source selection plays the version that was last watched.
/// A directly queried alternate version keeps its own source first. Per-user playback position
/// is not surfaced on the source itself; it is carried by each version's own UserData.
/// </summary>
/// <param name="item">The queried item.</param>
/// <param name="sources">The item's media sources.</param>
@@ -451,16 +451,6 @@ namespace Emby.Server.Implementations.Library
}
}
foreach (var source in sources)
{
if (source.Id is not null
&& dataBySourceId.TryGetValue(source.Id, out var data)
&& data.PlaybackPositionTicks > 0)
{
source.PlaybackPositionTicks = data.PlaybackPositionTicks;
}
}
// Reorder only for a resumable (in-progress) version;
// a completed version has no position to resume, so it must not be pulled to the front here.
var resumeSource = VersionPlaybackSelector.SelectMostRecentlyPlayed(

View File

@@ -53,6 +53,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IItemQueryHelpers _queryHelpers;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MovieSimilarItemsProvider"/> class.
@@ -60,14 +61,17 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
/// <param name="dbProvider">The database context factory.</param>
/// <param name="queryHelpers">The shared query helpers.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <param name="libraryManager">The library manager.</param>
public MovieSimilarItemsProvider(
IDbContextFactory<JellyfinDbContext> dbProvider,
IItemQueryHelpers queryHelpers,
IServerConfigurationManager serverConfigurationManager)
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager)
{
_dbProvider = dbProvider;
_queryHelpers = queryHelpers;
_serverConfigurationManager = serverConfigurationManager;
_libraryManager = libraryManager;
}
/// <inheritdoc/>
@@ -156,6 +160,11 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
IsPlayed = false
};
if (query.User is not null)
{
_libraryManager.ConfigureUserAccess(filter, query.User);
}
_queryHelpers.PrepareFilterQuery(filter);
var baseQuery = _queryHelpers.PrepareItemQuery(context, filter);
baseQuery = _queryHelpers.TranslateQuery(baseQuery, context, filter);

View File

@@ -106,5 +106,7 @@
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"CleanupUserDataTask": "Tarea de limpieza de datos de usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos de usuario (estado de visualización, favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días."
"CleanupUserDataTaskDescription": "Limpia todos los datos de usuario (estado de visualización, favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"LyricDownloadFailureFromForItem": "No se pudo descargar las letras de {0} para {1}",
"Original": "Original"
}

View File

@@ -106,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
"Original": "מקור"
}

View File

@@ -106,5 +106,7 @@
"TaskMoveTrickplayImages": "遷移快轉縮圖位置",
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。",
"CleanupUserDataTask": "用戶資料清理工作",
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。"
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。",
"Original": "原作",
"LyricDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的歌詞"
}

View File

@@ -385,7 +385,7 @@ namespace Emby.Server.Implementations.Localization
// Convert ints directly
// This may override some of the locale specific age ratings (but those always map to the same age)
if (int.TryParse(rating, out var ratingAge))
if (TryParseRatingAsScore(rating, out var ratingAge))
{
return new(ratingAge, null);
}
@@ -487,6 +487,13 @@ namespace Emby.Server.Implementations.Localization
return true;
}
// If it's not a recognized rating string, fall back to using the number as the score
if (TryParseRatingAsScore(ratingPart, out var numericScore))
{
result = new ParentalRatingScore(numericScore, null);
return true;
}
_logger.LogWarning(
"Rating '{Rating}' not found in the '{CountryCode}' rating system, treating as unrated",
rating,
@@ -501,6 +508,18 @@ namespace Emby.Server.Implementations.Localization
return true;
}
/// <summary>
/// Tries to parse a rating as a number, allowing an optional trailing '+' (e.g. "16" or "18+").
/// </summary>
/// <param name="ratingValue">Rating value to parse.</param>
/// <param name="score">Parsed score.</param>
/// <returns>Returns true if parsing was successful.</returns>
private static bool TryParseRatingAsScore(ReadOnlySpan<char> ratingValue, out int score)
{
var trimmed = ratingValue.TrimEnd('+');
return int.TryParse(trimmed, out score);
}
/// <inheritdoc />
public string GetLocalizedString(string phrase)
{

View File

@@ -10,21 +10,42 @@
}
},
{
"ratingStrings": ["K12"],
"ratingStrings": ["K12", "K-12", "12"],
"ratingScore": {
"score": 12,
"subScore": null
}
},
{
"ratingStrings": ["K13", "K-13", "13"],
"ratingScore": {
"score": 13,
"subScore": null
}
},
{
"ratingStrings": ["K15"],
"ratingStrings": ["K15", "K-15", "15"],
"ratingScore": {
"score": 15,
"subScore": null
}
},
{
"ratingStrings": ["K18"],
"ratingStrings": ["K16", "K-16", "16"],
"ratingScore": {
"score": 16,
"subScore": null
}
},
{
"ratingStrings": ["K17", "K-17", "17"],
"ratingScore": {
"score": 17,
"subScore": null
}
},
{
"ratingStrings": ["K18", "K-18", "18", "18+"],
"ratingScore": {
"score": 18,
"subScore": null

View File

@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -234,7 +235,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
FileName = _mediaEncoder.EncoderPath,
Arguments = args,
RedirectStandardOutput = false,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true
},
})

View File

@@ -60,11 +60,8 @@ public class HlsSegmentController : BaseJellyfinApiController
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())));
if (file is null)
{
return BadRequest("Invalid segment.");
}
@@ -86,12 +83,9 @@ public class HlsSegmentController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
var file = ValidateTranscodePath(string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())));
if (file is null
|| !Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid segment.");
}
@@ -140,18 +134,13 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())));
if (file is null)
{
return BadRequest("Invalid segment.");
}
var normalizedPlaylistId = playlistId;
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
// Add . to start of segment container for future use.
segmentContainer = segmentContainer.Insert(0, ".");
@@ -161,7 +150,7 @@ public class HlsSegmentController : BaseJellyfinApiController
var pathExtension = Path.GetExtension(path);
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
&& path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase))
&& path.Contains(playlistId, StringComparison.OrdinalIgnoreCase))
{
playlistPath = path;
break;
@@ -173,6 +162,19 @@ public class HlsSegmentController : BaseJellyfinApiController
: GetFileResult(file, playlistPath);
}
private string? ValidateTranscodePath(string filename)
{
var transcodePath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_serverConfigurationManager.GetTranscodePath()));
var file = Path.GetFullPath(filename, transcodePath);
// Require a separator after the transcode path so a sibling like "<transcodePath>-evil" can't pass.
if (!file.StartsWith(transcodePath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
return null;
}
return file;
}
private ActionResult GetFileResult(string path, string playlistPath)
{
var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);

View File

@@ -279,6 +279,11 @@ public class ItemUpdateController : BaseJellyfinApiController
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
}
if (request.SeriesName is not null && item is IHasSeries hasSeries)
{
hasSeries.SeriesName = request.SeriesName;
}
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
item.ProductionYear = request.ProductionYear;
@@ -297,6 +302,8 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (var season in rseries.Children.OfType<Season>())
{
season.SeriesName = rseries.Name;
if (!season.LockedFields.Contains(MetadataField.OfficialRating))
{
season.OfficialRating = request.OfficialRating;
@@ -314,6 +321,8 @@ public class ItemUpdateController : BaseJellyfinApiController
foreach (var ep in season.Children.OfType<Episode>())
{
ep.SeriesName = rseries.Name;
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
{
ep.OfficialRating = request.OfficialRating;

View File

@@ -316,7 +316,6 @@ public class ItemsController : BaseJellyfinApiController
if (folder is IHasCollectionType hasCollectionType)
{
collectionType = hasCollectionType.CollectionType;
includeItemTypes = [.. includeItemTypes.Union(DtoExtensions.GetBaseItemKindsForCollectionType(collectionType))];
}
if (collectionType == CollectionType.playlists)
@@ -329,7 +328,6 @@ public class ItemsController : BaseJellyfinApiController
includeItemTypes = collectionType switch
{
CollectionType.boxsets => [BaseItemKind.BoxSet],
null => [BaseItemKind.Movie, BaseItemKind.Series],
_ => []
};
}

View File

@@ -226,10 +226,13 @@ public class PluginsController : BaseJellyfinApiController
return NotFound();
}
if (!string.IsNullOrEmpty(plugin.Manifest.ImagePath))
string? imagePath = plugin.Manifest.ImagePath;
if (!string.IsNullOrWhiteSpace(imagePath))
{
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
if (!System.IO.File.Exists(imagePath))
var pluginPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(plugin.Path));
imagePath = Path.GetFullPath(imagePath, pluginPath);
// Require a separator after the plugin path so a sibling like "<pluginPath>-evil" can't pass.
if (imagePath.StartsWith(pluginPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) is false || System.IO.File.Exists(imagePath) is false)
{
return NotFound();
}

View File

@@ -551,8 +551,6 @@ public class UserLibraryController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
dtoOptions.PreferEpisodeParentPoster = true;
var list = _userViewManager.GetLatestItems(
new LatestItemsQuery
{

View File

@@ -631,6 +631,7 @@ namespace Jellyfin.Server.Implementations.Users
if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
{
user.SetPermission(PermissionKind.IsDisabled, true);
dbContext.Update(user);
await dbContext.SaveChangesAsync()
.ConfigureAwait(false);
await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);

View File

@@ -48,6 +48,7 @@ namespace Jellyfin.Server.Extensions
c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API");
c.InjectStylesheet($"/{baseUrl}api-docs/swagger/custom.css");
c.RoutePrefix = "api-docs/swagger";
c.UseRequestInterceptor("""(req) => { req.headers['Authorization'] = `MediaBrowser Token=\"${req.headers['Authorization']}\"`; return req; }""");
})
.UseReDoc(c =>
{

View File

@@ -1,19 +1,29 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// The result of a channel item query.
/// </summary>
public class ChannelItemResult
{
/// <summary>
/// Initializes a new instance of the <see cref="ChannelItemResult"/> class.
/// </summary>
public ChannelItemResult()
{
Items = Array.Empty<ChannelItemInfo>();
}
/// <summary>
/// Gets or sets the items.
/// </summary>
public IReadOnlyList<ChannelItemInfo> Items { get; set; }
/// <summary>
/// Gets or sets the total record count.
/// </summary>
public int? TotalRecordCount { get; set; }
}
}

View File

@@ -1,11 +1,18 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// The type of a channel item.
/// </summary>
public enum ChannelItemType
{
/// <summary>
/// The item is a media item.
/// </summary>
Media = 0,
/// <summary>
/// The item is a folder.
/// </summary>
Folder = 1
}
}

View File

@@ -1,11 +1,15 @@
#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// The request for a latest media search in a channel.
/// </summary>
public class ChannelLatestMediaSearch
{
/// <summary>
/// Gets or sets the user id.
/// </summary>
public string UserId { get; set; }
}
}

View File

@@ -1,17 +1,33 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// The parental rating of a channel.
/// </summary>
public enum ChannelParentalRating
{
/// <summary>
/// Suitable for a general audience.
/// </summary>
GeneralAudience = 0,
/// <summary>
/// Parental guidance suggested (US PG).
/// </summary>
UsPG = 1,
/// <summary>
/// Parents strongly cautioned (US PG-13).
/// </summary>
UsPG13 = 2,
/// <summary>
/// Restricted (US R).
/// </summary>
UsR = 3,
/// <summary>
/// Suitable for adults only.
/// </summary>
Adult = 4
}
}

View File

@@ -1,13 +1,20 @@
#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// The request for a search in a channel.
/// </summary>
public class ChannelSearchInfo
{
/// <summary>
/// Gets or sets the search term.
/// </summary>
public string SearchTerm { get; set; }
/// <summary>
/// Gets or sets the user id.
/// </summary>
public string UserId { get; set; }
}
}

View File

@@ -1,14 +1,15 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// Interface for channels that provide a cache key.
/// </summary>
public interface IHasCacheKey
{
/// <summary>
/// Gets the cache key.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>System.String.</returns>
/// <returns>The cache key.</returns>
string? GetCacheKey(string? userId);
}
}

View File

@@ -1,15 +1,27 @@
#pragma warning disable CS1591
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// Interface for channels that support deleting items.
/// </summary>
public interface ISupportsDelete
{
/// <summary>
/// Gets a value indicating whether the item can be deleted.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if the item can be deleted, <c>false</c> otherwise.</returns>
bool CanDelete(BaseItem item);
/// <summary>
/// Deletes the item with the provided id.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the deletion of the item.</returns>
Task DeleteItem(string id, CancellationToken cancellationToken);
}
}

View File

@@ -1,11 +1,12 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Channels
{
/// <summary>
/// Interface for channels that support retrieving the latest media.
/// </summary>
public interface ISupportsLatestMedia
{
/// <summary>

View File

@@ -81,13 +81,6 @@ namespace MediaBrowser.Controller.Dto
/// </summary>
public bool AddCurrentProgram { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an episode's portrait poster (its season's primary
/// image, falling back to the series') should replace the episode's own (16:9) primary image.
/// Used by views that render episodes as poster cards, e.g. "Latest".
/// </summary>
public bool PreferEpisodeParentPoster { get; set; }
/// <summary>
/// Gets a value indicating whether the specified field is populated.
/// </summary>

View File

@@ -492,8 +492,8 @@ namespace MediaBrowser.Controller.Entities
public IOrderedEnumerable<Video> GetAdditionalParts(User user = null)
{
return GetAdditionalPartIds()
.Select(i => LibraryManager.GetItemById<Video>(i, user))
.Where(i => i is not null)
.Select(i => LibraryManager.GetItemById<Video>(i))
.Where(i => i is not null && (user is null || i.IsVisible(user)))
.OrderBy(i => i.SortName);
}

View File

@@ -4054,7 +4054,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter);
}
var outFormat = doCuTonemap ? "yuv420p10le" : "yuv420p";
var outFormat = doCuTonemap ? "p010le" : "yuv420p";
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);

View File

@@ -1,11 +1,15 @@
#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// The lookup info for books.
/// </summary>
public class BookInfo : ItemLookupInfo
{
/// <summary>
/// Gets or sets the name of the series the book belongs to.
/// </summary>
public string SeriesName { get; set; }
}
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// The lookup info for box sets.
/// </summary>
public class BoxSetInfo : ItemLookupInfo
{
}

View File

@@ -1,10 +1,16 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// Interface for items that provide lookup info for metadata providers.
/// </summary>
/// <typeparam name="TLookupInfoType">The type of the lookup info.</typeparam>
public interface IHasLookupInfo<out TLookupInfoType>
where TLookupInfoType : ItemLookupInfo, new()
{
/// <summary>
/// Gets the lookup info.
/// </summary>
/// <returns>The lookup info.</returns>
TLookupInfoType GetLookupInfo();
}
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// Marker interface for custom metadata providers that run before the regular metadata refresh.
/// </summary>
public interface IPreRefreshProvider : ICustomMetadataProvider
{
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// The lookup info for movies.
/// </summary>
public class MovieInfo : ItemLookupInfo
{
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// The lookup info for persons.
/// </summary>
public class PersonLookupInfo : ItemLookupInfo
{
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// The lookup info for series.
/// </summary>
public class SeriesInfo : ItemLookupInfo
{
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// The lookup info for trailers.
/// </summary>
public class TrailerInfo : ItemLookupInfo
{
}

View File

@@ -84,7 +84,7 @@ namespace MediaBrowser.LocalMetadata.Images
if (item.SupportsLocalMetadata)
{
// Episode has its own provider
if (item is Episode || item is Audio || item is Photo)
if (item is Episode || (item is Audio && item is not AudioBook) || item is Photo)
{
return false;
}

View File

@@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
@@ -184,8 +185,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ "libavdevice", new Version(58, 13) },
{ "libavfilter", new Version(7, 110) },
{ "libswscale", new Version(5, 9) },
{ "libswresample", new Version(3, 9) },
{ "libpostproc", new Version(55, 9) }
{ "libswresample", new Version(3, 9) }
};
private readonly ILogger _logger;
@@ -645,7 +645,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
RedirectStandardInput = redirectStandardIn,
StandardOutputEncoding = Encoding.UTF8,
RedirectStandardOutput = true,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true
}
})

View File

@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
@@ -528,6 +529,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
UseShellExecute = false,
// Must consume both or ffmpeg may hang due to deadlocks.
StandardOutputEncoding = Encoding.UTF8,
RedirectStandardOutput = true,
FileName = _ffprobePath,

View File

@@ -167,7 +167,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
if (fileInfo.Protocol == MediaProtocol.Http)
{
var result = await DetectCharset(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false);
var result = await DetectCharset(fileInfo.Path, cancellationToken).ConfigureAwait(false);
var detected = result.Detected;
if (detected is not null)
@@ -445,98 +445,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
encodingParam = " -sub_charenc " + encodingParam;
}
int exitCode;
var args = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath);
using (var process = new Process
{
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
Arguments = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
EnableRaisingEvents = true
})
{
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
{
process.Kill(true);
exitCode = -1;
}
}
var failed = false;
if (exitCode == -1)
{
failed = true;
if (File.Exists(outputPath))
{
try
{
_logger.LogInformation("Deleting converted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
}
}
}
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
{
failed = true;
try
{
_logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (FileNotFoundException)
{
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
}
}
if (failed)
{
_logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath);
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath));
}
await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
await ExtractSubtitlesForFile(
inputPath,
args,
[outputPath],
cancellationToken).ConfigureAwait(false);
WriteCacheMeta(outputPath, inputPath);
_logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
}
private string GetExtractableSubtitleFormat(MediaStream subtitleStream)
@@ -727,7 +644,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var outputPaths = new List<string>();
var args = string.Format(
CultureInfo.InvariantCulture,
"-i {0}",
"-y -i {0}",
inputPath);
foreach (var subtitleStream in subtitleStreams)
@@ -781,50 +698,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private async Task ExtractSubtitlesForFile(
string inputPath,
string args,
List<string> outputPaths,
IReadOnlyList<string> outputPaths,
CancellationToken cancellationToken)
{
int exitCode;
using (var process = new Process
{
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
Arguments = args,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
EnableRaisingEvents = true
})
{
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
{
process.Kill(true);
exitCode = -1;
}
}
var (exitCode, ffmpegError) = await RunSubtitleExtractionProcess(args, cancellationToken).ConfigureAwait(false);
var failed = false;
@@ -884,6 +761,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (failed)
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.IsNullOrWhiteSpace(ffmpegError))
{
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath}: {FfmpegOutput}", inputPath, ffmpegError);
}
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath));
}
@@ -941,16 +825,38 @@ namespace MediaBrowser.MediaEncoding.Subtitles
ArgumentException.ThrowIfNullOrEmpty(outputPath);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var processArgs = string.Format(
CultureInfo.InvariantCulture,
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
"-y -i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath,
subtitleStreamIndex,
outputCodec,
outputPath);
await ExtractSubtitlesForFile(
inputPath,
processArgs,
[outputPath],
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Runs ffmpeg to extract or convert subtitles, capturing its exit code and stderr output.
/// </summary>
/// <remarks>
/// stdin is redirected and closed, and <c>-nostdin</c> is prepended to the arguments, so ffmpeg can never
/// block reading an inherited stdin handle (which happens when Jellyfin runs as a service, e.g. under NSSM,
/// and stalls subtitle extraction until the timeout). stderr is redirected and drained so a full pipe buffer
/// cannot deadlock ffmpeg and so its output can be surfaced on failure; stdout is left un-redirected as it is
/// unused for subtitle extraction.
/// </remarks>
/// <param name="arguments">The ffmpeg command line arguments.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The ffmpeg exit code (-1 on timeout) and its captured stderr output.</returns>
private async Task<(int ExitCode, string StandardError)> RunSubtitleExtractionProcess(string arguments, CancellationToken cancellationToken)
{
int exitCode;
var standardError = string.Empty;
using (var process = new Process
{
@@ -958,8 +864,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
FileName = _mediaEncoder.EncoderPath,
Arguments = processArgs,
Arguments = "-nostdin " + arguments,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
@@ -975,14 +883,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
// Close stdin so ffmpeg observes EOF instead of blocking on an inherited handle.
process.StandardInput.Close();
// Begin draining stderr before waiting for exit; a full stderr pipe buffer would otherwise deadlock ffmpeg.
var standardErrorTask = process.StandardError.ReadToEndAsync(CancellationToken.None);
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
using var waitSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
waitSource.CancelAfter(TimeSpan.FromMinutes(timeoutMinutes));
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
await process.WaitForExitAsync(waitSource.Token).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
@@ -990,59 +905,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
process.Kill(true);
exitCode = -1;
}
}
var failed = false;
if (exitCode == -1)
{
failed = true;
try
{
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
standardError = await standardErrorTask.ConfigureAwait(false);
}
catch (FileNotFoundException)
catch (OperationCanceledException)
{
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
}
}
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
{
failed = true;
try
{
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (FileNotFoundException)
{
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
// Reading ffmpeg output was cancelled; nothing more to capture.
}
}
if (failed)
{
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath));
}
_logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
{
await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
}
return (exitCode, standardError);
}
/// <summary>
@@ -1104,7 +978,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
var result = await DetectCharset(path, cancellationToken).ConfigureAwait(false);
var charset = result.Detected?.EncodingName ?? string.Empty;
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
@@ -1120,8 +994,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return charset;
}
private async Task<DetectionResult> DetectCharset(string path, MediaProtocol protocol, CancellationToken cancellationToken)
private async Task<DetectionResult> DetectCharset(string path, CancellationToken cancellationToken)
{
var protocol = _mediaSourceManager.GetPathProtocol(path);
switch (protocol)
{
case MediaProtocol.Http:
@@ -1141,7 +1016,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
default:
throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol");
throw new NotSupportedException($"Unsupported protocol: {protocol}");
}
}

View File

@@ -424,6 +424,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
// Must consume both stdout and stderr or deadlocks may occur
// RedirectStandardOutput = true,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,

View File

@@ -1,11 +1,23 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
{
/// <summary>
/// The codec type of a codec profile.
/// </summary>
public enum CodecType
{
/// <summary>
/// The profile applies to a video codec.
/// </summary>
Video = 0,
/// <summary>
/// The profile applies to the audio codec of a video stream.
/// </summary>
VideoAudio = 1,
/// <summary>
/// The profile applies to an audio codec.
/// </summary>
Audio = 2
}
}

View File

@@ -1,10 +1,18 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
{
/// <summary>
/// The encoding context.
/// </summary>
public enum EncodingContext
{
/// <summary>
/// The media is transcoded on the fly and delivered as a stream.
/// </summary>
Streaming = 0,
/// <summary>
/// The media is transcoded to a static file.
/// </summary>
Static = 1
}
}

View File

@@ -1,11 +1,23 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
{
/// <summary>
/// The playback error code.
/// </summary>
public enum PlaybackErrorCode
{
/// <summary>
/// Playback of the item is not allowed.
/// </summary>
NotAllowed = 0,
/// <summary>
/// No stream compatible with the device profile was found.
/// </summary>
NoCompatibleStream = 1,
/// <summary>
/// The rate limit has been exceeded.
/// </summary>
RateLimitExceeded = 2
}
}

View File

@@ -1,11 +1,18 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
{
/// <summary>
/// The resolution constraints.
/// </summary>
public class ResolutionOptions
{
/// <summary>
/// Gets or sets the maximum width.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets the maximum height.
/// </summary>
public int? MaxHeight { get; set; }
}
}

View File

@@ -1,10 +1,18 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
{
/// <summary>
/// The transcode seek info.
/// </summary>
public enum TranscodeSeekInfo
{
/// <summary>
/// The seek method is chosen automatically.
/// </summary>
Auto = 0,
/// <summary>
/// Seeking is performed by byte position.
/// </summary>
Bytes = 1
}
}

View File

@@ -55,11 +55,6 @@ namespace MediaBrowser.Model.Dto
public long? RunTimeTicks { get; set; }
/// <summary>
/// Gets or sets the playback position for this specific source.
/// </summary>
public long? PlaybackPositionTicks { get; set; }
public bool ReadAtNativeFramerate { get; set; }
public bool IgnoreDts { get; set; }

View File

@@ -1,17 +1,28 @@
#nullable disable
#pragma warning disable CS1591
using System.ComponentModel.DataAnnotations;
namespace MediaBrowser.Model.Session
{
/// <summary>
/// A command to display a message on a client.
/// </summary>
public class MessageCommand
{
/// <summary>
/// Gets or sets the message header.
/// </summary>
public string Header { get; set; }
/// <summary>
/// Gets or sets the message text.
/// </summary>
[Required(AllowEmptyStrings = false)]
public string Text { get; set; }
/// <summary>
/// Gets or sets the timeout in milliseconds after which the message should be dismissed.
/// </summary>
public long? TimeoutMs { get; set; }
}
}

View File

@@ -1,11 +1,23 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Session
{
/// <summary>
/// The play method.
/// </summary>
public enum PlayMethod
{
/// <summary>
/// The media is transcoded before it is sent to the client.
/// </summary>
Transcode = 0,
/// <summary>
/// The media is remuxed into a compatible container but the streams are not re-encoded.
/// </summary>
DirectStream = 1,
/// <summary>
/// The media is sent to the client as-is.
/// </summary>
DirectPlay = 2
}
}

View File

@@ -1,11 +1,18 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Session
{
/// <summary>
/// A request to change the playstate of a session.
/// </summary>
public class PlaystateRequest
{
/// <summary>
/// Gets or sets the playstate command.
/// </summary>
public PlaystateCommand Command { get; set; }
/// <summary>
/// Gets or sets the seek position in ticks.
/// </summary>
public long? SeekPositionTicks { get; set; }
/// <summary>

View File

@@ -1,13 +1,21 @@
#nullable disable
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Model.Session;
/// <summary>
/// An item in a play queue.
/// </summary>
public record QueueItem
{
/// <summary>
/// Gets or sets the item id.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the playlist item id.
/// </summary>
public string PlaylistItemId { get; set; }
}

View File

@@ -1,11 +1,23 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Session
{
/// <summary>
/// The repeat mode of a play queue.
/// </summary>
public enum RepeatMode
{
/// <summary>
/// Nothing is repeated.
/// </summary>
RepeatNone = 0,
/// <summary>
/// The whole queue is repeated.
/// </summary>
RepeatAll = 1,
/// <summary>
/// The current item is repeated.
/// </summary>
RepeatOne = 2
}
}

View File

@@ -90,7 +90,7 @@ namespace MediaBrowser.Providers.Manager
{
ArgumentException.ThrowIfNullOrEmpty(mimeType);
var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && item is not Audio;
var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && (item is AudioBook || item is not Audio);
if (type != ImageType.Primary && item is Episode)
{

View File

@@ -680,11 +680,18 @@ namespace MediaBrowser.Providers.Manager
return providers;
}
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, MetadataRefreshOptions options)
{
// Get providers to refresh
var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
// When identifying, run the provider the user picked first so the correct image is used.
if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
{
providers = providers
.OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.OrdinalIgnoreCase) ? 0 : 1);
}
var dateLastImageRefresh = item.DateLastRefreshed;
// Run all if either of these flags are true

View File

@@ -256,11 +256,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.Overview = seriesResult.Overview;
var studios = Enumerable.Empty<string>();
if (seriesResult.Networks is not null)
{
series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray();
studios = studios.Concat(seriesResult.Networks.Select(i => i.Name).OfType<string>());
}
if (seriesResult.ProductionCompanies is not null)
{
studios = studios.Concat(seriesResult.ProductionCompanies.Select(i => i.Name).OfType<string>());
}
series.SetStudios(studios);
if (seriesResult.Genres is not null)
{
series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray();

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
@@ -48,6 +49,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
PersonKind.Producer
};
/// <summary>
/// Writing jobs to keep.
/// </summary>
private static readonly FrozenSet<string> _writerJobs = new[]
{
"writer",
"screenplay",
"novel"
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
[GeneratedRegex(@"[\W_-[·]]+")]
private static partial Regex NonWordRegex();
@@ -82,7 +93,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase)))
&& crew.Job is not null && _writerJobs.Contains(crew.Job))
{
return PersonKind.Writer;
}

View File

@@ -83,6 +83,7 @@ namespace Jellyfin.LiveTv.IO
CreateNoWindow = true,
UseShellExecute = false,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true,
RedirectStandardInput = true,

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
namespace Jellyfin.MediaEncoding.Keyframes.FfProbe;
@@ -31,6 +32,7 @@ public static class FfProbeKeyframeExtractor
CreateNoWindow = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden,

View File

@@ -0,0 +1,161 @@
using System;
using System.IO;
using Jellyfin.Api.Controllers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers;
// The legacy HLS endpoints build a file path from caller-supplied route values, and the audio
// and video segment endpoints are not authenticated. These tests pin down that requests escaping
// the transcode directory are rejected while legitimate ones still serve a file.
public sealed class HlsSegmentControllerTests
{
private readonly Mock<IFileSystem> _fileSystem = new();
private readonly Mock<IServerConfigurationManager> _config = new();
private readonly Mock<ITranscodeManager> _transcodeManager = new();
private readonly string _transcodePath;
public HlsSegmentControllerTests()
{
_transcodePath = Path.Combine(Path.GetTempPath(), "jellyfin-hls-segment-tests");
Directory.CreateDirectory(_transcodePath);
_config.Setup(c => c.GetConfiguration("encoding"))
.Returns(new EncodingOptions { TranscodingTempPath = _transcodePath });
_config.SetupGet(c => c.CommonApplicationPaths).Returns(Mock.Of<IApplicationPaths>());
}
private HlsSegmentController CreateController(string requestPath)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Path = requestPath;
return new HlsSegmentController(_fileSystem.Object, _config.Object, _transcodeManager.Object)
{
ControllerContext = new ControllerContext { HttpContext = httpContext }
};
}
[Fact]
public void GetHlsAudioSegmentLegacy_SegmentInsideTranscodePath_ReturnsFile()
{
var controller = CreateController("/Audio/abc/hls/segment/stream.mp3");
var result = controller.GetHlsAudioSegmentLegacy("abc", "segment");
Assert.IsType<PhysicalFileResult>(result);
}
[Theory]
[InlineData("../../../../etc/passwd")]
[InlineData("subdir/../../../../etc/passwd")]
public void GetHlsAudioSegmentLegacy_TraversalOutsideTranscodePath_ReturnsBadRequest(string segmentId)
{
var controller = CreateController("/Audio/abc/hls/segment/stream.mp3");
var result = controller.GetHlsAudioSegmentLegacy("abc", segmentId);
Assert.IsType<BadRequestObjectResult>(result);
}
[Fact]
public void GetHlsAudioSegmentLegacy_AbsoluteRootedPath_ReturnsBadRequest()
{
var controller = CreateController("/Audio/abc/hls/segment/stream.mp3");
// A rooted segment id makes Path.GetFullPath discard the transcode base.
var rooted = OperatingSystem.IsWindows() ? "C:\\Windows\\win.ini" : "/etc/passwd";
var result = controller.GetHlsAudioSegmentLegacy("abc", rooted);
Assert.IsType<BadRequestObjectResult>(result);
}
[Fact]
public void GetHlsAudioSegmentLegacy_SiblingPrefixDirectory_ReturnsBadRequest()
{
var controller = CreateController("/Audio/abc/hls/segment/stream.mp3");
// Resolves to "<transcodePath>-evil/passwd", which shares the transcode path as a string prefix.
var result = controller.GetHlsAudioSegmentLegacy("abc", "../jellyfin-hls-segment-tests-evil/passwd");
Assert.IsType<BadRequestObjectResult>(result);
}
[Fact]
public void GetHlsPlaylistLegacy_M3u8InsideTranscodePath_ReturnsFile()
{
var controller = CreateController("/Videos/abc/hls/list/stream.m3u8");
var result = controller.GetHlsPlaylistLegacy("abc", "list");
Assert.IsType<PhysicalFileResult>(result);
}
[Fact]
public void GetHlsPlaylistLegacy_NonPlaylistExtension_ReturnsBadRequest()
{
// Playlist endpoint serves only .m3u8, even for a path inside the transcode dir.
var controller = CreateController("/Videos/abc/hls/list/stream.mp4");
var result = controller.GetHlsPlaylistLegacy("abc", "list");
Assert.IsType<BadRequestObjectResult>(result);
}
[Theory]
[InlineData("../../../../etc/passwd")]
public void GetHlsPlaylistLegacy_TraversalOutsideTranscodePath_ReturnsBadRequest(string playlistId)
{
var controller = CreateController("/Videos/abc/hls/list/stream.m3u8");
var result = controller.GetHlsPlaylistLegacy("abc", playlistId);
Assert.IsType<BadRequestObjectResult>(result);
}
[Fact]
public void GetHlsVideoSegmentLegacy_SegmentInsideTranscodePath_ReturnsFile()
{
_fileSystem.Setup(f => f.GetFilePaths(_transcodePath, false))
.Returns(new[] { Path.Combine(_transcodePath, "playlist123.ts") });
var controller = CreateController("/Videos/abc/hls/playlist123/seg1.ts");
var result = controller.GetHlsVideoSegmentLegacy("abc", "playlist123", "seg1", "ts");
Assert.IsType<PhysicalFileResult>(result);
}
[Fact]
public void GetHlsVideoSegmentLegacy_NoMatchingPlaylist_ReturnsNotFound()
{
_fileSystem.Setup(f => f.GetFilePaths(_transcodePath, false))
.Returns(Array.Empty<string>());
var controller = CreateController("/Videos/abc/hls/playlist123/seg1.ts");
var result = controller.GetHlsVideoSegmentLegacy("abc", "playlist123", "seg1", "ts");
Assert.IsType<NotFoundObjectResult>(result);
}
[Theory]
[InlineData("../../../../etc/passwd")]
public void GetHlsVideoSegmentLegacy_TraversalOutsideTranscodePath_ReturnsBadRequest(string segmentId)
{
var controller = CreateController("/Videos/abc/hls/playlist123/seg1.ts");
var result = controller.GetHlsVideoSegmentLegacy("abc", "playlist123", segmentId, "ts");
Assert.IsType<BadRequestObjectResult>(result);
_fileSystem.Verify(f => f.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.IO;
using Jellyfin.Api.Controllers;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers;
// Covers the path-traversal validation in GetPluginImage: a plugin's manifest ImagePath
// must resolve to a file inside the plugin's own directory.
public sealed class PluginsControllerTests
{
private readonly Mock<IPluginManager> _pluginManager = new();
private readonly string _pluginPath;
public PluginsControllerTests()
{
_pluginPath = Path.Combine(Path.GetTempPath(), "jellyfin-plugin-image-tests");
Directory.CreateDirectory(_pluginPath);
}
private PluginsController CreateController() =>
new PluginsController(Mock.Of<IInstallationManager>(), _pluginManager.Object)
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
private void SetupPlugin(Guid id, Version version, string? imagePath)
{
var manifest = new PluginManifest { Id = id, Name = "Test", Version = version.ToString(), ImagePath = imagePath };
_pluginManager.Setup(p => p.GetPlugin(id, version))
.Returns(new LocalPlugin(_pluginPath, true, manifest));
}
[Fact]
public void GetPluginImage_UnknownPlugin_ReturnsNotFound()
{
var result = CreateController().GetPluginImage(Guid.NewGuid(), new Version(1, 0));
Assert.IsType<NotFoundResult>(result);
}
[Fact]
public void GetPluginImage_ImageInsidePluginPath_ReturnsFile()
{
var id = Guid.NewGuid();
var version = new Version(1, 0);
File.WriteAllBytes(Path.Combine(_pluginPath, "logo.png"), Array.Empty<byte>());
SetupPlugin(id, version, "logo.png");
var result = CreateController().GetPluginImage(id, version);
Assert.IsType<PhysicalFileResult>(result);
}
[Fact]
public void GetPluginImage_ImageInsidePluginPathButMissing_ReturnsNotFound()
{
var id = Guid.NewGuid();
var version = new Version(1, 0);
SetupPlugin(id, version, "does-not-exist.png");
var result = CreateController().GetPluginImage(id, version);
Assert.IsType<NotFoundResult>(result);
}
[Theory]
[InlineData("../../../../etc/passwd")]
[InlineData("subdir/../../../../etc/passwd")]
public void GetPluginImage_TraversalOutsidePluginPath_ReturnsNotFound(string imagePath)
{
var id = Guid.NewGuid();
var version = new Version(1, 0);
SetupPlugin(id, version, imagePath);
var result = CreateController().GetPluginImage(id, version);
Assert.IsType<NotFoundResult>(result);
}
[Fact]
public void GetPluginImage_SiblingPrefixDirectory_ReturnsNotFound()
{
var id = Guid.NewGuid();
var version = new Version(1, 0);
// Resolves to "<pluginPath>-evil/logo.png", which shares the plugin path as a string prefix.
// The file is created so the check fails on the boundary, not on File.Exists.
var siblingDir = _pluginPath + "-evil";
Directory.CreateDirectory(siblingDir);
File.WriteAllBytes(Path.Combine(siblingDir, "logo.png"), Array.Empty<byte>());
SetupPlugin(id, version, "../jellyfin-plugin-image-tests-evil/logo.png");
var result = CreateController().GetPluginImage(id, version);
Assert.IsType<NotFoundResult>(result);
}
[Fact]
public void GetPluginImage_AbsoluteImagePath_ReturnsNotFound()
{
var id = Guid.NewGuid();
var version = new Version(1, 0);
SetupPlugin(id, version, OperatingSystem.IsWindows() ? "C:\\Windows\\win.ini" : "/etc/passwd");
var result = CreateController().GetPluginImage(id, version);
Assert.IsType<NotFoundResult>(result);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void GetPluginImage_NoImagePathOrResource_ReturnsNotFound(string? imagePath)
{
var id = Guid.NewGuid();
var version = new Version(1, 0);
SetupPlugin(id, version, imagePath);
var result = CreateController().GetPluginImage(id, version);
Assert.IsType<NotFoundResult>(result);
}
}

View File

@@ -24,6 +24,7 @@ namespace Jellyfin.MediaEncoding.Tests
[InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV432Output, false)]
[InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, true)]
[InlineData(EncoderValidatorTestsData.FFmpegGitWithoutLibpostprocOutput, true)]
[InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)]
public void ValidateVersionInternalTest(string versionOutput, bool valid)
{
@@ -41,6 +42,7 @@ namespace Jellyfin.MediaEncoding.Tests
Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegGitWithoutLibpostprocOutput, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput, null);
}
}

View File

@@ -86,6 +86,15 @@ libswscale 5. 9.100 / 5. 9.100
libswresample 3. 9.100 / 3. 9.100
libpostproc 55. 9.100 / 55. 9.100";
public const string FFmpegGitWithoutLibpostprocOutput = @"ffmpeg version N-122128-gdeadbeef Copyright (c) 2000-2026 the FFmpeg developers
libavutil 60. 26.102 / 60. 26.102
libavcodec 62. 28.102 / 62. 28.102
libavformat 62. 12.102 / 62. 12.102
libavdevice 62. 3.102 / 62. 3.102
libavfilter 11. 14.102 / 11. 14.102
libswscale 9. 5.102 / 9. 5.102
libswresample 6. 3.102 / 6. 3.102";
public const string FFmpegGitUnknownOutput = @"ffmpeg version N-45325-gb173e0353-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2018 the FFmpeg developers
built with gcc 6.3.0 (Debian 6.3.0-18+deb9u1) 20170516
configuration: --enable-gpl --enable-version3 --enable-static --disable-debug --disable-ffplay --disable-indev=sndio --disable-outdev=sndio --cc=gcc-6 --enable-fontconfig --enable-frei0r --enable-gnutls --enable-gray --enable-libfribidi --enable-libass --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librubberband --enable-libsoxr --enable-libspeex --enable-libvorbis --enable-libopus --enable-libtheora --enable-libvidstab --enable-libvo-amrwbenc --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libzimg

View File

@@ -11,6 +11,7 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -56,57 +57,63 @@ public class DtoServiceTests
}
[Fact]
public void GetBaseItemDto_PreferEpisodeParentPoster_PrefersSeasonPosterOverEpisodeAndSeries()
public void GetBaseItemDto_Episode_AttachesSeasonPosterAsParentPrimaryImage()
{
var (episode, season, series) = BuildEpisode(seasonHasPoster: true);
var options = new DtoOptions(false) { PreferEpisodeParentPoster = true };
var (episode, season, _) = BuildEpisode(seasonHasPoster: true);
var options = new DtoOptions(false) { Fields = [ItemFields.PrimaryImageAspectRatio] };
var dto = _dtoService.GetBaseItemDto(episode, options);
// The episode's own 16:9 primary is dropped in favor of the season's portrait poster.
Assert.False(dto.ImageTags is not null && dto.ImageTags.ContainsKey(ImageType.Primary));
Assert.Null(dto.SeriesPrimaryImageTag);
// The season poster is attached additively; the episode keeps its own primary and 16:9 ratio,
// and clients decide per view whether to prefer the parent/series poster over the episode still.
Assert.NotNull(dto.ImageTags);
Assert.True(dto.ImageTags.ContainsKey(ImageType.Primary));
Assert.NotNull(dto.SeriesPrimaryImageTag);
Assert.Equal(season.Id, dto.ParentPrimaryImageItemId);
Assert.Equal("tag:" + season.GetImageInfo(ImageType.Primary, 0)!.Path, dto.ParentPrimaryImageTag);
// Aspect ratio follows the (portrait) poster, not the episode's 16:9 image.
Assert.Equal(season.GetDefaultPrimaryImageAspectRatio(), dto.PrimaryImageAspectRatio);
// Aspect ratio stays the episode's own image, not the poster's.
Assert.Equal(episode.GetDefaultPrimaryImageAspectRatio(), dto.PrimaryImageAspectRatio);
}
[Fact]
public void GetBaseItemDto_PreferEpisodeParentPoster_FallsBackToSeriesWhenSeasonHasNoPoster()
public void GetBaseItemDto_Episode_ParentPrimaryImageFallsBackToSeriesWhenSeasonHasNoPoster()
{
var (episode, _, series) = BuildEpisode(seasonHasPoster: false);
var options = new DtoOptions(false) { PreferEpisodeParentPoster = true };
var options = new DtoOptions(false);
var dto = _dtoService.GetBaseItemDto(episode, options);
Assert.False(dto.ImageTags is not null && dto.ImageTags.ContainsKey(ImageType.Primary));
Assert.Null(dto.SeriesPrimaryImageTag);
// Episode image is retained; ParentPrimaryImage falls back to the series poster.
Assert.NotNull(dto.ImageTags);
Assert.True(dto.ImageTags.ContainsKey(ImageType.Primary));
Assert.NotNull(dto.SeriesPrimaryImageTag);
Assert.Equal(series.Id, dto.ParentPrimaryImageItemId);
Assert.Equal("tag:" + series.GetImageInfo(ImageType.Primary, 0)!.Path, dto.ParentPrimaryImageTag);
}
[Fact]
public void GetBaseItemDto_WithoutPreferEpisodeParentPoster_KeepsEpisodePrimary()
public void GetBaseItemDto_Episode_WithoutParentPosters_KeepsOnlyEpisodePrimary()
{
var (episode, _, _) = BuildEpisode(seasonHasPoster: true);
var (episode, _, _) = BuildEpisode(seasonHasPoster: false, seriesHasPoster: false);
var options = new DtoOptions(false);
var dto = _dtoService.GetBaseItemDto(episode, options);
// Default behavior: the episode keeps its own primary and exposes the series poster as a tag.
// With no season or series poster there is nothing to attach; the episode keeps its own primary.
Assert.NotNull(dto.ImageTags);
Assert.True(dto.ImageTags.ContainsKey(ImageType.Primary));
Assert.NotNull(dto.SeriesPrimaryImageTag);
Assert.Null(dto.ParentPrimaryImageItemId);
}
private (Episode Episode, Season Season, Series Series) BuildEpisode(bool seasonHasPoster)
private (Episode Episode, Season Season, Series Series) BuildEpisode(bool seasonHasPoster, bool seriesHasPoster = true)
{
// Non-local (http) paths keep aspect-ratio resolution off the image processor and on the
// item's default ratio, which is portrait (2/3) for Season/Series and 16:9 for Episode.
var series = new Series { Id = Guid.NewGuid(), Name = "Series" };
series.SetImage(new ItemImageInfo { Type = ImageType.Primary, Path = "http://test/series.jpg" }, 0);
if (seriesHasPoster)
{
series.SetImage(new ItemImageInfo { Type = ImageType.Primary, Path = "http://test/series.jpg" }, 0);
}
var season = new Season { Id = Guid.NewGuid(), Name = "Season", SeriesId = series.Id };
if (seasonHasPoster)

View File

@@ -150,7 +150,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
}
[Fact]
public void GetStaticMediaSources_PrimaryQueried_PopulatesPerVersionPositionsAndDefaultsToMostRecent()
public void GetStaticMediaSources_PrimaryQueried_DefaultsToMostRecentlyPlayedVersion()
{
var (primary, alt1, alt2) = SetupVersionGroup();
SetupUserDataBatch(new Dictionary<Guid, UserItemData>
@@ -161,12 +161,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library
var sources = _mediaSourceManager.GetStaticMediaSources(primary, false, _user);
// Each version carries its own resume point; the primary has none.
Assert.Equal((long?)10, sources.First(s => s.Id == alt1.Id.ToString("N")).PlaybackPositionTicks);
Assert.Equal((long?)20, sources.First(s => s.Id == alt2.Id.ToString("N")).PlaybackPositionTicks);
Assert.Null(sources.First(s => s.Id == primary.Id.ToString("N")).PlaybackPositionTicks);
// The most recently played version is the default source, so resuming plays the right file.
// Per-user positions live in each version's UserData, not on the source.
Assert.Equal(alt2.Id.ToString("N"), sources[0].Id);
}
@@ -182,9 +178,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library
var sources = _mediaSourceManager.GetStaticMediaSources(alt1, false, _user);
// An explicitly opened version keeps its own source first, even when a sibling was
// played more recently, but the sibling's resume point is still populated.
// played more recently.
Assert.Equal(alt1.Id.ToString("N"), sources[0].Id);
Assert.Equal((long?)20, sources.First(s => s.Id == alt2.Id.ToString("N")).PlaybackPositionTicks);
Assert.Equal(3, sources.Count);
}
@@ -197,7 +192,6 @@ namespace Jellyfin.Server.Implementations.Tests.Library
var sources = _mediaSourceManager.GetStaticMediaSources(primary, false, _user);
Assert.Equal(primary.Id.ToString("N"), sources[0].Id);
Assert.All(sources, s => Assert.Null(s.PlaybackPositionTicks));
}
[Fact]