mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-08 03:12:08 +01:00
Compare commits
1 Commits
renovate/m
...
allow-tmdb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86804686a4 |
8
.github/ISSUE_TEMPLATE/issue report.yml
vendored
8
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -87,9 +87,13 @@ body:
|
||||
label: Jellyfin Server version
|
||||
description: What version of Jellyfin are you using?
|
||||
options:
|
||||
- 10.11.8
|
||||
- 10.11.7
|
||||
- 10.11.6
|
||||
- 10.11.5
|
||||
- 10.11.4
|
||||
- 10.11.3
|
||||
- 10.11.2
|
||||
- 10.11.1
|
||||
- 10.11.0
|
||||
- Master
|
||||
- Unstable
|
||||
- Older*
|
||||
|
||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -28,13 +28,13 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
|
||||
@@ -1,28 +1,118 @@
|
||||
name: OpenAPI Publish
|
||||
name: OpenAPI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
publish-openapi:
|
||||
name: OpenAPI - Publish Artifact
|
||||
uses: ./.github/workflows/openapi-generate.yml
|
||||
openapi-head:
|
||||
name: OpenAPI - HEAD
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||
|
||||
openapi-base:
|
||||
name: OpenAPI - BASE
|
||||
if: ${{ github.base_ref != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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: Setup .NET
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||
|
||||
openapi-diff:
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
repository: ${{ github.repository }}
|
||||
artifact: openapi-head
|
||||
pull-requests: write
|
||||
|
||||
name: OpenAPI - Difference
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
|
||||
- name: Detect OpenAPI changes
|
||||
id: openapi-diff
|
||||
uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0
|
||||
with:
|
||||
old-spec: openapi-base/openapi.json
|
||||
new-spec: openapi-head/openapi.json
|
||||
markdown: openapi-changelog.md
|
||||
add-pr-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
publish-unstable:
|
||||
name: OpenAPI - Publish Unstable Spec
|
||||
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-openapi
|
||||
- openapi-head
|
||||
steps:
|
||||
- name: Set unstable dated version
|
||||
id: version
|
||||
@@ -83,7 +173,7 @@ jobs:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-openapi
|
||||
- openapi-head
|
||||
steps:
|
||||
- name: Set version number
|
||||
id: version
|
||||
44
.github/workflows/openapi-generate.yml
vendored
44
.github/workflows/openapi-generate.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: OpenAPI Generate
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
required: true
|
||||
type: string
|
||||
repository:
|
||||
required: true
|
||||
type: string
|
||||
artifact:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Main
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
repository: ${{ inputs.repository }}
|
||||
|
||||
- name: Configure .NET
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Create File
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
80
.github/workflows/openapi-pull-request.yml
vendored
80
.github/workflows/openapi-pull-request.yml
vendored
@@ -1,80 +0,0 @@
|
||||
name: OpenAPI Check
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ancestor:
|
||||
name: Common Ancestor
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
base_ref: ${{ steps.ancestor.outputs.base_ref }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
- name: Search History
|
||||
id: ancestor
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
||||
git 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 }} HEAD)
|
||||
|
||||
echo "ref: ${ANCESTOR_REF}"
|
||||
|
||||
echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
head:
|
||||
name: Head Artifact
|
||||
uses: ./.github/workflows/openapi-generate.yml
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
artifact: openapi-head
|
||||
|
||||
base:
|
||||
name: Base Artifact
|
||||
uses: ./.github/workflows/openapi-generate.yml
|
||||
needs:
|
||||
- ancestor
|
||||
with:
|
||||
ref: ${{ needs.ancestor.outputs.base_ref }}
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
artifact: openapi-base
|
||||
|
||||
diff:
|
||||
name: Generate Report
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- head
|
||||
- base
|
||||
steps:
|
||||
- name: Download Head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download Base
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
- name: Detect Changes
|
||||
id: openapi-diff
|
||||
run: |
|
||||
sed -i 's:allOf:oneOf:g' openapi-head/openapi.json
|
||||
sed -i 's:allOf:oneOf:g' openapi-base/openapi.json
|
||||
|
||||
mkdir -p /tmp/openapi-report
|
||||
mv openapi-head/openapi.json /tmp/openapi-report/head.json
|
||||
mv openapi-base/openapi.json /tmp/openapi-report/base.json
|
||||
|
||||
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: openapi-report
|
||||
path: /tmp/openapi-report/openapi-report.md
|
||||
59
.github/workflows/openapi-workflow-run.yml
vendored
59
.github/workflows/openapi-workflow-run.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: OpenAPI Report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- OpenAPI Check
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Generate Metadata
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
pr_number: ${{ steps.pr_number.outputs.pr_number }}
|
||||
steps:
|
||||
- name: Get Pull Request Number
|
||||
id: pr_number
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number)
|
||||
PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number')
|
||||
|
||||
echo "repository: ${GITHUB_REPOSITORY}"
|
||||
echo "sha: ${HEAD_SHA}"
|
||||
echo "response: ${API_RESPONSE}"
|
||||
echo "pr: ${PR_NUMBER}"
|
||||
|
||||
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
comment:
|
||||
name: Pull Request Comment
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
needs:
|
||||
- metadata
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download OpenAPI Report
|
||||
id: download_report
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-report
|
||||
path: openapi-report
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Push Comment
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md
|
||||
pr-number: ${{ needs.metadata.outputs.pr_number }}
|
||||
comment-tag: openapi-report
|
||||
2
.github/workflows/release-bump-version.yaml
vendored
2
.github/workflows/release-bump-version.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
timeoutSeconds: 3600
|
||||
|
||||
- name: Setup YQ
|
||||
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest
|
||||
uses: chrisdickinson/setup-yq@latest
|
||||
with:
|
||||
yq-version: v4.9.8
|
||||
|
||||
|
||||
@@ -164,7 +164,6 @@
|
||||
- [XVicarious](https://github.com/XVicarious)
|
||||
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
||||
- [ZachPhelan](https://github.com/ZachPhelan)
|
||||
- [ZeusCraft10](https://github.com/ZeusCraft10)
|
||||
- [KristupasSavickas](https://github.com/KristupasSavickas)
|
||||
- [Pusta](https://github.com/pusta)
|
||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -75,11 +75,11 @@
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.5" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.12.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.11.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="3.0.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
||||
@@ -152,8 +152,8 @@ namespace Emby.Naming.Common
|
||||
|
||||
CleanStrings =
|
||||
[
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^(?<cleaned>.+?)(\[.*\])",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
|
||||
var match = expression.Match(name);
|
||||
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
|
||||
{
|
||||
newName = cleaned.Value.Trim();
|
||||
newName = cleaned.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -217,8 +217,6 @@ namespace Emby.Naming.Video
|
||||
// The CleanStringParser should have removed common keywords etc.
|
||||
return testFilename.IsEmpty
|
||||
|| testFilename[0] == '-'
|
||||
|| testFilename[0] == '_'
|
||||
|| testFilename[0] == '.'
|
||||
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1019,15 +1019,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
dto.AlbumId = albumParent.Id;
|
||||
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
|
||||
if (albumParent.LUFS.HasValue)
|
||||
{
|
||||
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
|
||||
dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
|
||||
}
|
||||
else if (albumParent.NormalizationGain.HasValue)
|
||||
{
|
||||
dto.AlbumNormalizationGain = albumParent.NormalizationGain;
|
||||
}
|
||||
}
|
||||
|
||||
// if (options.ContainsField(ItemFields.MediaSourceCount))
|
||||
|
||||
@@ -31,20 +31,6 @@ namespace Emby.Server.Implementations.Library
|
||||
"**/*.sample.?????",
|
||||
"**/sample/*",
|
||||
|
||||
// Avoid adding Hungarian sample files
|
||||
// https://github.com/jellyfin/jellyfin/issues/16237
|
||||
"**/minta.?",
|
||||
"**/minta.??",
|
||||
"**/minta.???", // Matches minta.mkv
|
||||
"**/minta.????", // Matches minta.webm
|
||||
"**/minta.?????",
|
||||
"**/*.minta.?",
|
||||
"**/*.minta.??",
|
||||
"**/*.minta.???",
|
||||
"**/*.minta.????",
|
||||
"**/*.minta.?????",
|
||||
"**/minta/*",
|
||||
|
||||
// Directories
|
||||
"**/metadata/**",
|
||||
"**/metadata",
|
||||
|
||||
@@ -70,6 +70,12 @@ namespace Emby.Server.Implementations.Library
|
||||
return match ? imdbId.ToString() : null;
|
||||
}
|
||||
|
||||
// Allow tmdb as an alias for tmdbid
|
||||
if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return str.GetAttributeValue("tmdb");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
public class BookResolver : ItemResolver<Book>
|
||||
{
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" };
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||
|
||||
protected override Book Resolve(ItemResolveArgs args)
|
||||
{
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"Photos": "Fotos",
|
||||
"Playlists": "Llistes de reproducció",
|
||||
"Plugin": "Complement",
|
||||
"PluginInstalledWithName": "{0} s'ha instal·lat",
|
||||
"PluginUninstalledWithName": "{0} s'ha desinstal·lat",
|
||||
"PluginInstalledWithName": "{0} ha estat instal·lat",
|
||||
"PluginUninstalledWithName": "S'ha instal·lat {0}",
|
||||
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
||||
"ProviderValue": "Proveïdor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"HeaderContinueWatching": "Weiterschauen",
|
||||
"HeaderFavoriteAlbums": "Lieblingsalben",
|
||||
"HeaderFavoriteArtists": "Lieblingsinterpreten",
|
||||
"HeaderFavoriteEpisodes": "Lieblingsfolgen",
|
||||
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
|
||||
"HeaderFavoriteShows": "Lieblingsserien",
|
||||
"HeaderFavoriteSongs": "Lieblingssongs",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
|
||||
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
|
||||
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
|
||||
"TaskExtractMediaSegments": "Skaneeri meedialõike",
|
||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
|
||||
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
|
||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
|
||||
|
||||
@@ -14,9 +14,5 @@
|
||||
"DeviceOnlineWithName": "{0} er sambundið",
|
||||
"Favorites": "Yndis",
|
||||
"Folders": "Mappur",
|
||||
"Forced": "Kravt",
|
||||
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
|
||||
"HeaderFavoriteEpisodes": "Yndispartar",
|
||||
"HeaderFavoriteSongs": "Yndissangir",
|
||||
"LabelIpAddressValue": "IP atsetur: {0}"
|
||||
"Forced": "Kravt"
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
|
||||
"TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
|
||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें।",
|
||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||
@@ -136,5 +136,5 @@
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
|
||||
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
||||
}
|
||||
|
||||
@@ -9,46 +9,46 @@
|
||||
"Artists": "არტისტი",
|
||||
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
|
||||
"Books": "წიგნები",
|
||||
"Forced": "იძულებითი",
|
||||
"Forced": "ძალით",
|
||||
"Inherit": "მემკვიდრეობით",
|
||||
"Latest": "უახლესი",
|
||||
"Movies": "ფილმები",
|
||||
"Music": "მუსიკა",
|
||||
"Photos": "ფოტოები",
|
||||
"Playlists": "დასაკრავი სიები",
|
||||
"Plugin": "მოდული",
|
||||
"Plugin": "დამატება",
|
||||
"Shows": "სერიალები",
|
||||
"Songs": "სიმღერები",
|
||||
"Sync": "სინქრონიზაცია",
|
||||
"System": "სისტემა",
|
||||
"Undefined": "განუსაზღვრელი",
|
||||
"Undefined": "აღუწერელი",
|
||||
"User": "მომხმარებელი",
|
||||
"TasksMaintenanceCategory": "რემონტი",
|
||||
"TasksLibraryCategory": "ბიბლიოთეკა",
|
||||
"ChapterNameValue": "თავი {0}",
|
||||
"HeaderContinueWatching": "ყურების გაგრძელება",
|
||||
"HeaderFavoriteArtists": "რჩეული შემსრულებლები",
|
||||
"DeviceOfflineWithName": "{0} გამოეთიშა",
|
||||
"DeviceOfflineWithName": "{0} გაითიშა",
|
||||
"External": "გარე",
|
||||
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
|
||||
"HeaderFavoriteSongs": "რჩეული სიმღერები",
|
||||
"HeaderRecordingGroups": "ჩამწერი ჯგუფები",
|
||||
"HearingImpaired": "სმენადაქვეითებული",
|
||||
"LabelRunningTimeValue": "ხანგრძლივობა: {0}",
|
||||
"LabelRunningTimeValue": "გაშვებულობის დრო: {0}",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
|
||||
"MixedContent": "შერეული შემცველობა",
|
||||
"MusicVideos": "მუსიკალური ვიდეოები",
|
||||
"MusicVideos": "მუსიკის ვიდეოები",
|
||||
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
|
||||
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
|
||||
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
|
||||
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
|
||||
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
|
||||
"PluginUninstalledWithName": "{0} წაიშალა",
|
||||
"ScheduledTaskStartedWithName": "{0} დაიწყო",
|
||||
"ScheduledTaskStartedWithName": "{0} გაეშვა",
|
||||
"VersionNumber": "ვერსია {0}",
|
||||
"TasksChannelsCategory": "ინტერნეტ-არხები",
|
||||
"ValueSpecialEpisodeName": "დამატებითი - {0}",
|
||||
"ValueSpecialEpisodeName": "სპეციალური - {0}",
|
||||
"TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
|
||||
"Channels": "არხები",
|
||||
"Collections": "კოლექციები",
|
||||
@@ -56,31 +56,31 @@
|
||||
"Favorites": "რჩეულები",
|
||||
"Folders": "საქაღალდეები",
|
||||
"HeaderFavoriteShows": "რჩეული სერიალები",
|
||||
"HeaderLiveTV": "ლაივ ტელევიზია",
|
||||
"HeaderNextUp": "შემდეგი",
|
||||
"HeaderLiveTV": "ცოცხალი TV",
|
||||
"HeaderNextUp": "შემდეგი ზემოთ",
|
||||
"HomeVideos": "სახლის ვიდეოები",
|
||||
"NameSeasonNumber": "სეზონი {0}",
|
||||
"NameSeasonUnknown": "სეზონი უცნობია",
|
||||
"NotificationOptionPluginError": "მოდულის შეცდომა",
|
||||
"NotificationOptionPluginInstalled": "მოდული დაყენებულია",
|
||||
"NotificationOptionPluginUninstalled": "მოდული წაიშალა",
|
||||
"NotificationOptionPluginError": "დამატების შეცდომა",
|
||||
"NotificationOptionPluginInstalled": "დამატება დაყენებულია",
|
||||
"NotificationOptionPluginUninstalled": "დამატება წაიშალა",
|
||||
"ProviderValue": "მომწოდებელი: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ვერ შესრულდა",
|
||||
"TvShows": "სატელევიზიო სერიალები",
|
||||
"ScheduledTaskFailedWithName": "{0} ავარიულია",
|
||||
"TvShows": "TV სერიალები",
|
||||
"TaskRefreshPeople": "ხალხის განახლება",
|
||||
"TaskUpdatePlugins": "მოდულების განახლება",
|
||||
"TaskUpdatePlugins": "დამატებების განახლება",
|
||||
"TaskRefreshChannels": "არხების განახლება",
|
||||
"TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია",
|
||||
"TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია",
|
||||
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
|
||||
"DeviceOnlineWithName": "{0} დაკავშირდა",
|
||||
"DeviceOnlineWithName": "{0} შეერთებულია",
|
||||
"LabelIpAddressValue": "IP მისამართი: {0}",
|
||||
"NameInstallFailed": "{0}-ის დაყენების შეცდომა",
|
||||
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
|
||||
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
|
||||
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
|
||||
"NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია",
|
||||
"NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა",
|
||||
"NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა",
|
||||
"NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია",
|
||||
"NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია",
|
||||
"NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა",
|
||||
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
|
||||
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
|
||||
"PluginInstalledWithName": "{0} დაყენებულია",
|
||||
@@ -91,51 +91,39 @@
|
||||
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
|
||||
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
|
||||
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
|
||||
"TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა",
|
||||
"UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს",
|
||||
"FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან",
|
||||
"TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა",
|
||||
"UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს",
|
||||
"FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა",
|
||||
"MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
|
||||
"MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
|
||||
"ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
|
||||
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
|
||||
"UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
|
||||
"UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან",
|
||||
"UserOfflineFromDevice": "{0}-ი {1}-დან გაეთიშა",
|
||||
"UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან",
|
||||
"UserOfflineFromDevice": "{0}-ი {1}-დან გაითიშა",
|
||||
"ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
|
||||
"ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
|
||||
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
|
||||
"UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე",
|
||||
"UserPasswordChangedWithName": "მომხმარებელი {0}-სთვის პაროლი შეიცვალა",
|
||||
"UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე",
|
||||
"UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
|
||||
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
|
||||
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
|
||||
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
|
||||
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
|
||||
"CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
|
||||
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა",
|
||||
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
|
||||
"ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
|
||||
"TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებს.",
|
||||
"TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებს.",
|
||||
"TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენს მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებს.",
|
||||
"TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.",
|
||||
"TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
|
||||
"TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.",
|
||||
"TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
|
||||
"TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
|
||||
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული მოდულების განახლებების გადმოწერა და დაყენება.",
|
||||
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
|
||||
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.",
|
||||
"TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
|
||||
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.",
|
||||
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება",
|
||||
"TaskAudioNormalization": "აუდიოს ნორმალიზება",
|
||||
"TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
|
||||
"TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
|
||||
"TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
|
||||
"TaskCleanCollectionsAndPlaylists": "კოლექციების და დასაკრავი სიების გასუფთავება",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "შლის არარსებულ ერთეულებს კოლექციებიდან და დასაკრავი სიებიდან.",
|
||||
"TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
|
||||
"TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
|
||||
"TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
|
||||
"TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.",
|
||||
"CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება",
|
||||
"CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ."
|
||||
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
|
||||
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
|
||||
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
|
||||
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Apparaat: {1}",
|
||||
"Application": "Applicatie",
|
||||
"Artists": "Artiesten",
|
||||
@@ -13,18 +14,19 @@
|
||||
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
|
||||
"Favorites": "Favorieten",
|
||||
"Folders": "Mappen",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Albumartiesten",
|
||||
"HeaderContinueWatching": "Verder kijken",
|
||||
"HeaderContinueWatching": "Verderkijken",
|
||||
"HeaderFavoriteAlbums": "Favoriete albums",
|
||||
"HeaderFavoriteArtists": "Favoriete artiesten",
|
||||
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
|
||||
"HeaderFavoriteShows": "Favoriete series",
|
||||
"HeaderFavoriteSongs": "Favoriete nummers",
|
||||
"HeaderLiveTV": "Live-tv",
|
||||
"HeaderNextUp": "Volgende",
|
||||
"HeaderNextUp": "Als volgende",
|
||||
"HeaderRecordingGroups": "Opnamegroepen",
|
||||
"HomeVideos": "Homevideo's",
|
||||
"Inherit": "Overnemen",
|
||||
"Inherit": "Erven",
|
||||
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
|
||||
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
|
||||
"LabelIpAddressValue": "IP-adres: {0}",
|
||||
@@ -114,7 +116,7 @@
|
||||
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
|
||||
"TaskCleanActivityLog": "Activiteitenlogboek legen",
|
||||
"Undefined": "Niet gedefinieerd",
|
||||
"Forced": "Geforceerd",
|
||||
"Forced": "Gedwongen",
|
||||
"Default": "Standaard",
|
||||
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
|
||||
"TaskOptimizeDatabase": "Database optimaliseren",
|
||||
@@ -135,7 +137,5 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
|
||||
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
|
||||
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
|
||||
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
|
||||
"Albums": "Albums",
|
||||
"Genres": "Genres"
|
||||
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
|
||||
}
|
||||
|
||||
@@ -124,17 +124,17 @@
|
||||
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
|
||||
"External": "Зовнішній",
|
||||
"HearingImpaired": "З порушеннями слуху",
|
||||
"TaskRefreshTrickplayImagesDescription": "Створює прев'ю-зображення для відео у ввімкнених медіатеках.",
|
||||
"TaskRefreshTrickplayImages": "Створити Прев'ю-зображення",
|
||||
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
|
||||
"TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
|
||||
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
|
||||
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
|
||||
"TaskAudioNormalization": "Нормалізація аудіо",
|
||||
"TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень",
|
||||
"TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень",
|
||||
"TaskMoveTrickplayImagesDescription": "Переміщує наявні прев'ю-зображення відповідно до налаштувань медіатеки.",
|
||||
"TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
|
||||
"TaskExtractMediaSegments": "Сканування медіа-сегментів",
|
||||
"TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень",
|
||||
"TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
|
||||
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
|
||||
"CleanupUserDataTask": "Завдання очищення даних користувача",
|
||||
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."
|
||||
|
||||
@@ -3,38 +3,38 @@
|
||||
"AppDeviceValues": "程式:{0},裝置:{1}",
|
||||
"Application": "應用程式",
|
||||
"Artists": "藝人",
|
||||
"AuthenticationSucceededWithUserName": "{0} 成功通過驗證",
|
||||
"AuthenticationSucceededWithUserName": "成功授權 {0}",
|
||||
"Books": "書籍",
|
||||
"CameraImageUploadedFrom": "{0} 已經成功上載咗一張新相",
|
||||
"CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相",
|
||||
"Channels": "頻道",
|
||||
"ChapterNameValue": "第 {0} 章",
|
||||
"Collections": "系列",
|
||||
"DeviceOfflineWithName": "{0} 斷開咗連線",
|
||||
"DeviceOnlineWithName": "{0} 連線咗",
|
||||
"DeviceOfflineWithName": "{0} 已中斷連接",
|
||||
"DeviceOnlineWithName": "{0} 已連接",
|
||||
"FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗",
|
||||
"Favorites": "心水",
|
||||
"Favorites": "我的最愛",
|
||||
"Folders": "資料夾",
|
||||
"Genres": "風格",
|
||||
"HeaderAlbumArtists": "專輯歌手",
|
||||
"HeaderContinueWatching": "繼續睇返",
|
||||
"HeaderFavoriteAlbums": "心水嘅專輯",
|
||||
"HeaderFavoriteArtists": "心水嘅藝人",
|
||||
"HeaderFavoriteEpisodes": "心水嘅劇集",
|
||||
"HeaderFavoriteShows": "心水嘅節目",
|
||||
"HeaderFavoriteSongs": "心水嘅歌曲",
|
||||
"HeaderContinueWatching": "繼續觀看",
|
||||
"HeaderFavoriteAlbums": "最愛的專輯",
|
||||
"HeaderFavoriteArtists": "最愛的藝人",
|
||||
"HeaderFavoriteEpisodes": "最愛的劇集",
|
||||
"HeaderFavoriteShows": "最愛的節目",
|
||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||
"HeaderLiveTV": "電視直播",
|
||||
"HeaderNextUp": "跟住落嚟",
|
||||
"HeaderNextUp": "繼續觀看",
|
||||
"HeaderRecordingGroups": "錄製組",
|
||||
"HomeVideos": "家庭影片",
|
||||
"Inherit": "繼承",
|
||||
"ItemAddedWithName": "{0} 經已加咗入媒體櫃",
|
||||
"ItemRemovedWithName": "{0} 經已由媒體櫃移除咗",
|
||||
"ItemAddedWithName": "{0} 經已加咗入媒體庫",
|
||||
"ItemRemovedWithName": "{0} 經已由媒體庫移除咗",
|
||||
"LabelIpAddressValue": "IP 地址:{0}",
|
||||
"LabelRunningTimeValue": "運行時間:{0}",
|
||||
"LabelRunningTimeValue": "運作時間:{0}",
|
||||
"Latest": "最新",
|
||||
"MessageApplicationUpdated": "Jellyfin 經已更新咗",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經已更新咗",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」已經更新咗",
|
||||
"MessageServerConfigurationUpdated": "伺服器設定經已更新咗",
|
||||
"MixedContent": "混合內容",
|
||||
"Movies": "電影",
|
||||
@@ -42,100 +42,100 @@
|
||||
"MusicVideos": "MV",
|
||||
"NameInstallFailed": "{0} 安裝失敗",
|
||||
"NameSeasonNumber": "第 {0} 季",
|
||||
"NameSeasonUnknown": "未知嘅季度",
|
||||
"NameSeasonUnknown": "未知的季度",
|
||||
"NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。",
|
||||
"NotificationOptionApplicationUpdateAvailable": "有得更新應用程式",
|
||||
"NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗",
|
||||
"NotificationOptionAudioPlayback": "開始播放音訊",
|
||||
"NotificationOptionAudioPlaybackStopped": "停咗播放音訊",
|
||||
"NotificationOptionCameraImageUploaded": "相機相片上載咗",
|
||||
"NotificationOptionApplicationUpdateAvailable": "有可用的更新",
|
||||
"NotificationOptionApplicationUpdateInstalled": "完成更新應用程式",
|
||||
"NotificationOptionAudioPlayback": "播放音訊",
|
||||
"NotificationOptionAudioPlaybackStopped": "停止播放音訊",
|
||||
"NotificationOptionCameraImageUploaded": "相片上傳",
|
||||
"NotificationOptionInstallationFailed": "安裝失敗",
|
||||
"NotificationOptionNewLibraryContent": "加咗新內容",
|
||||
"NotificationOptionPluginError": "外掛程式錯誤",
|
||||
"NotificationOptionPluginInstalled": "安裝外掛程式",
|
||||
"NotificationOptionPluginUninstalled": "解除安裝外掛程式",
|
||||
"NotificationOptionPluginUpdateInstalled": "外掛程式更新好咗",
|
||||
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
|
||||
"NotificationOptionTaskFailed": "排程工作失敗",
|
||||
"NotificationOptionUserLockedOut": "用家被鎖定咗",
|
||||
"NotificationOptionVideoPlayback": "開始播放影片",
|
||||
"NotificationOptionVideoPlaybackStopped": "停咗播放影片",
|
||||
"NotificationOptionNewLibraryContent": "新增媒體",
|
||||
"NotificationOptionPluginError": "插件錯誤",
|
||||
"NotificationOptionPluginInstalled": "安裝插件",
|
||||
"NotificationOptionPluginUninstalled": "解除安裝插件",
|
||||
"NotificationOptionPluginUpdateInstalled": "完成更新插件",
|
||||
"NotificationOptionServerRestartRequired": "伺服器需要重啟",
|
||||
"NotificationOptionTaskFailed": "排程工作執行失敗",
|
||||
"NotificationOptionUserLockedOut": "封鎖用戶",
|
||||
"NotificationOptionVideoPlayback": "播放影片",
|
||||
"NotificationOptionVideoPlaybackStopped": "停止播放影片",
|
||||
"Photos": "相片",
|
||||
"Playlists": "播放清單",
|
||||
"Plugin": "外掛程式",
|
||||
"PluginInstalledWithName": "裝好咗 {0}",
|
||||
"PluginUninstalledWithName": "剷走咗 {0}",
|
||||
"PluginUpdatedWithName": "更新好咗 {0}",
|
||||
"Plugin": "插件",
|
||||
"PluginInstalledWithName": "已安裝 {0}",
|
||||
"PluginUninstalledWithName": "已移除 {0}",
|
||||
"PluginUpdatedWithName": "已更新 {0}",
|
||||
"ProviderValue": "提供者:{0}",
|
||||
"ScheduledTaskFailedWithName": "{0} 執行失敗",
|
||||
"ScheduledTaskStartedWithName": "開始執行 {0}",
|
||||
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
|
||||
"Shows": "節目",
|
||||
"Songs": "歌曲",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,唔該稍後再試。",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。",
|
||||
"SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗",
|
||||
"Sync": "同步",
|
||||
"System": "系統",
|
||||
"TvShows": "電視節目",
|
||||
"User": "使用者",
|
||||
"UserCreatedWithName": "經已建立咗新使用者 {0}",
|
||||
"UserDeletedWithName": "使用者 {0} 經已被刪走",
|
||||
"User": "用戶",
|
||||
"UserCreatedWithName": "經已建立咗新用戶 {0}",
|
||||
"UserDeletedWithName": "用戶 {0} 已經被刪除",
|
||||
"UserDownloadingItemWithValues": "{0} 下載緊 {1}",
|
||||
"UserLockedOutWithName": "使用者 {0} 經已被鎖定",
|
||||
"UserLockedOutWithName": "用戶 {0} 經已被鎖定",
|
||||
"UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線",
|
||||
"UserOnlineFromDevice": "{0} 正喺 {1} 連線",
|
||||
"UserPasswordChangedWithName": "使用者 {0} 嘅密碼經已更改咗",
|
||||
"UserPolicyUpdatedWithName": "使用者 {0} 嘅權限經已更新咗",
|
||||
"UserPasswordChangedWithName": "用戶 {0} 嘅密碼已經更改咗",
|
||||
"UserPolicyUpdatedWithName": "用戶 {0} 嘅權限已經更新咗",
|
||||
"UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃",
|
||||
"ValueSpecialEpisodeName": "特輯 - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體庫",
|
||||
"ValueSpecialEpisodeName": "特典 - {0}",
|
||||
"VersionNumber": "版本 {0}",
|
||||
"TaskDownloadMissingSubtitles": "下載漏咗嘅字幕",
|
||||
"TaskUpdatePlugins": "更新外掛程式",
|
||||
"TaskDownloadMissingSubtitles": "下載欠缺字幕",
|
||||
"TaskUpdatePlugins": "更新插件",
|
||||
"TasksApplicationCategory": "應用程式",
|
||||
"TaskRefreshLibraryDescription": "掃描媒體櫃嚟搵新檔案,同時重新載入媒體詳細資料。",
|
||||
"TaskRefreshLibraryDescription": "掃描媒體庫嚟搵新檔案,同時重新載入元數據。",
|
||||
"TasksMaintenanceCategory": "維護",
|
||||
"TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。",
|
||||
"TaskDownloadMissingSubtitlesDescription": "根據元數據設定,喺網上幫你搵返啲欠缺嘅字幕。",
|
||||
"TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。",
|
||||
"TaskRefreshChannels": "重新載入頻道",
|
||||
"TaskCleanTranscodeDescription": "自動刪走超過一日嘅轉碼檔案。",
|
||||
"TaskCleanTranscode": "清理轉碼資料夾",
|
||||
"TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。",
|
||||
"TaskRefreshPeopleDescription": "更新媒體櫃入面演員同導演嘅媒體詳細資料。",
|
||||
"TaskCleanLogsDescription": "自動刪走超過 {0} 日嘅紀錄檔。",
|
||||
"TaskCleanLogs": "清理日誌資料夾",
|
||||
"TaskRefreshLibrary": "掃描媒體櫃",
|
||||
"TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。",
|
||||
"TaskCleanTranscode": "清理轉碼檔資料夾",
|
||||
"TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅插件進行下載同安裝。",
|
||||
"TaskRefreshPeopleDescription": "更新媒體庫入面演員同導演嘅元數據。",
|
||||
"TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。",
|
||||
"TaskCleanLogs": "清理紀錄檔資料夾",
|
||||
"TaskRefreshLibrary": "掃描媒體庫",
|
||||
"TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。",
|
||||
"TaskRefreshChapterImages": "擷取章節圖片",
|
||||
"TaskCleanCacheDescription": "刪走系統已經唔再需要嘅快取檔案。",
|
||||
"TaskCleanCache": "清理快取(Cache)資料夾",
|
||||
"TasksChannelsCategory": "網路頻道",
|
||||
"TasksLibraryCategory": "媒體櫃",
|
||||
"TaskRefreshChapterImages": "提取章節圖像",
|
||||
"TaskCleanCacheDescription": "刪除系統已經唔再需要嘅緩存檔案。",
|
||||
"TaskCleanCache": "清理緩存資料夾",
|
||||
"TasksChannelsCategory": "網絡頻道",
|
||||
"TasksLibraryCategory": "媒體庫",
|
||||
"TaskRefreshPeople": "重新載入人物",
|
||||
"TaskCleanActivityLog": "清理活動紀錄",
|
||||
"TaskCleanActivityLog": "清理活動記錄",
|
||||
"Undefined": "未定義",
|
||||
"Forced": "強制",
|
||||
"Default": "初始",
|
||||
"TaskOptimizeDatabaseDescription": "壓縮數據櫃並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據櫃嘅操作之後行呢個任務,或者可以提升效能。",
|
||||
"TaskOptimizeDatabase": "最佳化數據櫃",
|
||||
"TaskCleanActivityLogDescription": "刪走超過設定日期嘅活動記錄。",
|
||||
"Default": "預設",
|
||||
"TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體庫或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。",
|
||||
"TaskOptimizeDatabase": "最佳化數據庫",
|
||||
"TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。",
|
||||
"TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。",
|
||||
"TaskKeyframeExtractor": "關鍵影格提取器",
|
||||
"External": "外部",
|
||||
"HearingImpaired": "聽力障礙",
|
||||
"TaskRefreshTrickplayImages": "產生搜畫預覽圖",
|
||||
"TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體櫃影片製作快轉預覽圖。",
|
||||
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
|
||||
"TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體庫影片製作快轉預覽圖。",
|
||||
"TaskExtractMediaSegments": "掃描媒體分段資訊",
|
||||
"TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅外掛程式入面提取媒體片段。",
|
||||
"TaskDownloadMissingLyrics": "下載缺失嘅歌詞",
|
||||
"TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。",
|
||||
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
|
||||
"TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞",
|
||||
"TaskCleanCollectionsAndPlaylists": "清理媒體系列(Collections)同埋播放清單",
|
||||
"TaskAudioNormalization": "音訊同等化",
|
||||
"TaskAudioNormalizationDescription": "掃描檔案入面嘅音訊標准化(Audio Normalization)數據。",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體櫃設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。",
|
||||
"TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置",
|
||||
"CleanupUserDataTask": "清理使用者資料嘅任務",
|
||||
"CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體用家數據(包括觀看狀態、心水狀態等)。"
|
||||
"CleanupUserDataTask": "清理用戶資料嘅任務",
|
||||
"CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。"
|
||||
}
|
||||
|
||||
@@ -960,7 +960,7 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
|
||||
var tracksChanged = UpdatePlaybackSettings(user, info, data);
|
||||
if (tracksChanged)
|
||||
if (!tracksChanged)
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetLiveHlsStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -187,7 +187,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -206,8 +206,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -412,12 +412,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -427,7 +427,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -448,8 +448,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -585,12 +585,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -601,7 +601,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -620,8 +620,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -752,12 +752,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -767,7 +767,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -788,8 +788,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -921,12 +921,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -937,7 +937,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -956,8 +956,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1091,7 +1091,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromQuery, Required] long runtimeTicks,
|
||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||
[FromQuery] bool? @static,
|
||||
@@ -1099,12 +1099,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1114,7 +1114,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -1135,8 +1135,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1273,7 +1273,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromQuery, Required] long runtimeTicks,
|
||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||
[FromQuery] bool? @static,
|
||||
@@ -1281,12 +1281,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1297,7 +1297,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -1316,8 +1316,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -11,7 +11,6 @@ using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -271,17 +270,15 @@ public class ItemsController : BaseJellyfinApiController
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var item = _libraryManager.GetParentItem(parentId, userId);
|
||||
QueryResult<BaseItem> result;
|
||||
|
||||
if (includeItemTypes.Length == 1
|
||||
&& includeItemTypes[0] == BaseItemKind.BoxSet
|
||||
&& item is not BoxSet)
|
||||
&& includeItemTypes[0] == BaseItemKind.BoxSet)
|
||||
{
|
||||
parentId = null;
|
||||
item = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
var item = _libraryManager.GetParentItem(parentId, userId);
|
||||
QueryResult<BaseItem> result;
|
||||
|
||||
if (item is not Folder folder)
|
||||
{
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
|
||||
@@ -454,7 +454,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||
{
|
||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -976,7 +976,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Created tuner host returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
||||
[HttpPost("TunerHosts")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
||||
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
|
||||
@@ -988,7 +988,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="204">Tuner host deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("TunerHosts")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
||||
{
|
||||
@@ -1021,7 +1021,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Created listings provider returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||
[HttpPost("ListingProviders")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
||||
@@ -1047,7 +1047,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="204">Listing provider deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("ListingProviders")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||
{
|
||||
@@ -1080,7 +1080,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Available countries returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||
@@ -1101,7 +1101,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Channel mapping options returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
||||
[HttpGet("ChannelMappingOptions")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||
=> _listingsManager.GetChannelMappingOptions(providerId);
|
||||
@@ -1113,7 +1113,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Created channel mapping returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||
[HttpPost("ChannelMappings")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
|
||||
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
|
||||
@@ -1137,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||
[HttpGet("Tuners/Discover")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
||||
=> _tunerHostManager.DiscoverTuners(newDevicesOnly);
|
||||
@@ -1185,7 +1185,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public ActionResult GetLiveStreamFile(
|
||||
[FromRoute, Required] string streamId,
|
||||
[FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container)
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
|
||||
{
|
||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||
if (liveStreamInfo is null)
|
||||
|
||||
@@ -47,7 +47,6 @@ public class PersonsController : BaseJellyfinApiController
|
||||
/// <summary>
|
||||
/// Gets all persons.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">The search term.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
@@ -58,7 +57,6 @@ public class PersonsController : BaseJellyfinApiController
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="parentId">Optional. Specify this to localize the search to a specific library. Omit to use the root.</param>
|
||||
/// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
@@ -67,7 +65,6 @@ public class PersonsController : BaseJellyfinApiController
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
|
||||
@@ -78,7 +75,6 @@ public class PersonsController : BaseJellyfinApiController
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery] Guid? appearsInItemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
@@ -100,8 +96,6 @@ public class PersonsController : BaseJellyfinApiController
|
||||
User = user,
|
||||
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
|
||||
AppearsInItemId = appearsInItemId ?? Guid.Empty,
|
||||
ParentId = parentId,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit ?? 0
|
||||
});
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController
|
||||
[FromBody, Required] NewGroupRequestDto requestData)
|
||||
{
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim());
|
||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
|
||||
return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None));
|
||||
}
|
||||
|
||||
|
||||
@@ -101,13 +101,13 @@ public class UniversalAudioController : BaseJellyfinApiController
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] int? transcodingAudioChannels,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
|
||||
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
||||
[FromQuery] int? maxAudioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
|
||||
@@ -313,18 +313,18 @@ public class VideosController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -334,7 +334,7 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -355,8 +355,8 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -551,18 +551,18 @@ public class VideosController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -572,7 +572,7 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -593,8 +593,8 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -209,25 +209,6 @@ public class DynamicHlsHelper
|
||||
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
||||
}
|
||||
|
||||
// For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1),
|
||||
// add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant.
|
||||
// SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement).
|
||||
// The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+)
|
||||
// select it over the fallback when both have identical BANDWIDTH.
|
||||
// Only emit for clients that explicitly declared DOVI support to avoid breaking
|
||||
// non-compliant players that don't recognize dvh1/dav1 CODECS strings.
|
||||
if (state.VideoStream is not null
|
||||
&& state.VideoRequest is not null
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.VideoRangeType == VideoRangeType.DOVI
|
||||
&& state.VideoStream.DvProfile.HasValue
|
||||
&& state.VideoStream.DvLevel.HasValue
|
||||
&& state.GetRequestedRangeTypes(state.VideoStream.Codec)
|
||||
.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
}
|
||||
|
||||
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||
@@ -374,65 +355,6 @@ public class DynamicHlsHelper
|
||||
return playlistBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible
|
||||
/// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect
|
||||
/// DoVi from the manifest rather than relying on init segment inspection.
|
||||
/// </summary>
|
||||
/// <param name="builder">StringBuilder for the master playlist.</param>
|
||||
/// <param name="state">StreamState of the current stream.</param>
|
||||
/// <param name="url">Playlist URL for this variant.</param>
|
||||
/// <param name="bitrate">Bitrate for the BANDWIDTH field.</param>
|
||||
/// <param name="subtitleGroup">Subtitle group identifier, or null.</param>
|
||||
private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
|
||||
{
|
||||
var dvProfile = state.VideoStream.DvProfile;
|
||||
var dvLevel = state.VideoStream.DvLevel;
|
||||
if (dvProfile is null || dvLevel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playlistBuilder = new StringBuilder();
|
||||
playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
|
||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
|
||||
.Append(",AVERAGE-BANDWIDTH=")
|
||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
playlistBuilder.Append(",VIDEO-RANGE=PQ");
|
||||
|
||||
var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec);
|
||||
|
||||
string audioCodecs = string.Empty;
|
||||
if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
|
||||
{
|
||||
audioCodecs = GetPlaylistAudioCodecs(state);
|
||||
}
|
||||
|
||||
playlistBuilder.Append(",CODECS=\"")
|
||||
.Append(dvCodec);
|
||||
if (!string.IsNullOrEmpty(audioCodecs))
|
||||
{
|
||||
playlistBuilder.Append(',').Append(audioCodecs);
|
||||
}
|
||||
|
||||
playlistBuilder.Append('"');
|
||||
|
||||
AppendPlaylistResolutionField(playlistBuilder, state);
|
||||
AppendPlaylistFramerateField(playlistBuilder, state);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||
{
|
||||
playlistBuilder.Append(",SUBTITLES=\"")
|
||||
.Append(subtitleGroup)
|
||||
.Append('"');
|
||||
}
|
||||
|
||||
playlistBuilder.AppendLine();
|
||||
playlistBuilder.AppendLine(url);
|
||||
builder.Append(playlistBuilder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a VIDEO-RANGE field containing the range of the output video stream.
|
||||
/// </summary>
|
||||
|
||||
@@ -346,25 +346,4 @@ public static class HlsCodecStringHelpers
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Dolby Vision codec string for profiles without a compatible base layer.
|
||||
/// </summary>
|
||||
/// <param name="dvProfile">Dolby Vision profile number.</param>
|
||||
/// <param name="dvLevel">Dolby Vision level number.</param>
|
||||
/// <param name="codec">Video codec name (e.g. "hevc", "av1") to determine the DoVi FourCC.</param>
|
||||
/// <returns>Dolby Vision codec string.</returns>
|
||||
public static string GetDoviString(int dvProfile, int dvLevel, string codec)
|
||||
{
|
||||
// HEVC DoVi uses dvh1, AV1 DoVi uses dav1 (out-of-band parameter sets, recommended by Apple HLS spec Rule 1.10)
|
||||
var fourCc = string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
|
||||
StringBuilder result = new StringBuilder(fourCc, 12);
|
||||
|
||||
result.Append('.')
|
||||
.AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvProfile)
|
||||
.Append('.')
|
||||
.AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvLevel);
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Streaming;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Api.Helpers;
|
||||
@@ -420,18 +422,14 @@ public static class StreamingHelpers
|
||||
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
case 4:
|
||||
if (videoRequest is not null && IsValidCodecName(val))
|
||||
if (videoRequest is not null)
|
||||
{
|
||||
videoRequest.VideoCodec = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 5:
|
||||
if (IsValidCodecName(val))
|
||||
{
|
||||
request.AudioCodec = val;
|
||||
}
|
||||
|
||||
request.AudioCodec = val;
|
||||
break;
|
||||
case 6:
|
||||
if (videoRequest is not null)
|
||||
@@ -485,7 +483,7 @@ public static class StreamingHelpers
|
||||
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 15:
|
||||
if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val))
|
||||
if (videoRequest is not null)
|
||||
{
|
||||
videoRequest.Level = val;
|
||||
}
|
||||
@@ -506,7 +504,7 @@ public static class StreamingHelpers
|
||||
|
||||
break;
|
||||
case 18:
|
||||
if (videoRequest is not null && IsValidCodecName(val))
|
||||
if (videoRequest is not null)
|
||||
{
|
||||
videoRequest.Profile = val;
|
||||
}
|
||||
@@ -565,11 +563,7 @@ public static class StreamingHelpers
|
||||
|
||||
break;
|
||||
case 30:
|
||||
if (IsValidCodecName(val))
|
||||
{
|
||||
request.SubtitleCodec = val;
|
||||
}
|
||||
|
||||
request.SubtitleCodec = val;
|
||||
break;
|
||||
case 31:
|
||||
if (videoRequest is not null)
|
||||
@@ -592,11 +586,6 @@ public static class StreamingHelpers
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidCodecName(string val)
|
||||
{
|
||||
return EncodingHelper.ContainerValidationRegex().IsMatch(val);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the container into its file extension.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Jellyfin.Api.Models.SyncPlayDtos;
|
||||
|
||||
/// <summary>
|
||||
@@ -19,6 +17,5 @@ public class NewGroupRequestDto
|
||||
/// Gets or sets the group name.
|
||||
/// </summary>
|
||||
/// <value>The name of the new group.</value>
|
||||
[StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")]
|
||||
public string GroupName { get; set; }
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ public static class UserEntityExtensions
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
|
||||
|
||||
@@ -118,21 +118,15 @@ public class BackupService : IBackupService
|
||||
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
|
||||
}
|
||||
|
||||
void CopyDirectory(string source, string target, string[]? exclude = null)
|
||||
void CopyDirectory(string source, string target)
|
||||
{
|
||||
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
|
||||
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
|
||||
var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
|
||||
foreach (var item in zipArchive.Entries)
|
||||
{
|
||||
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
|
||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||
|
||||
if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
||||
|| Path.EndsInDirectorySeparator(item.FullName))
|
||||
@@ -148,10 +142,8 @@ public class BackupService : IBackupService
|
||||
}
|
||||
|
||||
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
|
||||
CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
|
||||
CopyDirectory("Data", _applicationPaths.DataPath);
|
||||
CopyDirectory("Root", _applicationPaths.RootFolderPath);
|
||||
CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
|
||||
CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
|
||||
|
||||
if (manifest.Options.Database)
|
||||
{
|
||||
@@ -412,15 +404,6 @@ public class BackupService : IBackupService
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
|
||||
// If a custom metadata path is configured, the default location may still contain data.
|
||||
if (!string.Equals(
|
||||
Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
|
||||
Path.GetFullPath(_applicationPaths.InternalMetadataPath),
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default"));
|
||||
}
|
||||
}
|
||||
|
||||
var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -62,11 +62,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
|
||||
|
||||
if (filter.StartIndex.HasValue && filter.StartIndex > 0)
|
||||
{
|
||||
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
|
||||
}
|
||||
|
||||
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
|
||||
if (filter.Limit > 0)
|
||||
{
|
||||
dbQuery = dbQuery.Take(filter.Limit);
|
||||
@@ -201,11 +197,6 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
|
||||
}
|
||||
|
||||
if (filter.ParentId != null)
|
||||
{
|
||||
query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentId && i.ItemId == w.ItemId)));
|
||||
}
|
||||
|
||||
if (!filter.AppearsInItemId.IsEmpty())
|
||||
{
|
||||
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
|
||||
|
||||
@@ -182,18 +182,6 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var provider in _segmentProviders)
|
||||
{
|
||||
try
|
||||
{
|
||||
await provider.CleanupExtractedData(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Provider {ProviderName} failed to clean up extracted data for item {ItemId}", provider.Name, itemId);
|
||||
}
|
||||
}
|
||||
|
||||
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (db.ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -399,72 +399,64 @@ public class TrickplayManager : ITrickplayManager
|
||||
var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(workDir);
|
||||
|
||||
try
|
||||
var trickplayInfo = new TrickplayInfo
|
||||
{
|
||||
var trickplayInfo = new TrickplayInfo
|
||||
Width = width,
|
||||
Interval = options.Interval,
|
||||
TileWidth = options.TileWidth,
|
||||
TileHeight = options.TileHeight,
|
||||
ThumbnailCount = images.Count,
|
||||
// Set during image generation
|
||||
Height = 0,
|
||||
Bandwidth = 0
|
||||
};
|
||||
|
||||
/*
|
||||
* Generate trickplay tiles from sets of thumbnails
|
||||
*/
|
||||
var imageOptions = new ImageCollageOptions
|
||||
{
|
||||
Width = trickplayInfo.TileWidth,
|
||||
Height = trickplayInfo.TileHeight
|
||||
};
|
||||
|
||||
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
|
||||
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
|
||||
|
||||
for (int i = 0; i < requiredTiles; i++)
|
||||
{
|
||||
// Set output/input paths
|
||||
var tilePath = Path.Combine(workDir, $"{i}.jpg");
|
||||
|
||||
imageOptions.OutputPath = tilePath;
|
||||
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
|
||||
|
||||
// Generate image and use returned height for tiles info
|
||||
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
|
||||
if (trickplayInfo.Height == 0)
|
||||
{
|
||||
Width = width,
|
||||
Interval = options.Interval,
|
||||
TileWidth = options.TileWidth,
|
||||
TileHeight = options.TileHeight,
|
||||
ThumbnailCount = images.Count,
|
||||
// Set during image generation
|
||||
Height = 0,
|
||||
Bandwidth = 0
|
||||
};
|
||||
|
||||
/*
|
||||
* Generate trickplay tiles from sets of thumbnails
|
||||
*/
|
||||
var imageOptions = new ImageCollageOptions
|
||||
{
|
||||
Width = trickplayInfo.TileWidth,
|
||||
Height = trickplayInfo.TileHeight
|
||||
};
|
||||
|
||||
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
|
||||
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
|
||||
|
||||
for (int i = 0; i < requiredTiles; i++)
|
||||
{
|
||||
// Set output/input paths
|
||||
var tilePath = Path.Combine(workDir, $"{i}.jpg");
|
||||
|
||||
imageOptions.OutputPath = tilePath;
|
||||
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
|
||||
|
||||
// Generate image and use returned height for tiles info
|
||||
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
|
||||
if (trickplayInfo.Height == 0)
|
||||
{
|
||||
trickplayInfo.Height = height;
|
||||
}
|
||||
|
||||
// Update bitrate
|
||||
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
|
||||
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
|
||||
trickplayInfo.Height = height;
|
||||
}
|
||||
|
||||
/*
|
||||
* Move trickplay tiles to output directory
|
||||
*/
|
||||
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
|
||||
|
||||
// Replace existing tiles if they already exist
|
||||
if (Directory.Exists(outputDir))
|
||||
{
|
||||
Directory.Delete(outputDir, true);
|
||||
}
|
||||
|
||||
_fileSystem.MoveDirectory(workDir, outputDir);
|
||||
|
||||
return trickplayInfo;
|
||||
// Update bitrate
|
||||
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
|
||||
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
|
||||
}
|
||||
catch
|
||||
|
||||
/*
|
||||
* Move trickplay tiles to output directory
|
||||
*/
|
||||
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
|
||||
|
||||
// Replace existing tiles if they already exist
|
||||
if (Directory.Exists(outputDir))
|
||||
{
|
||||
Directory.Delete(workDir, true);
|
||||
throw;
|
||||
Directory.Delete(outputDir, true);
|
||||
}
|
||||
|
||||
_fileSystem.MoveDirectory(workDir, outputDir);
|
||||
|
||||
return trickplayInfo;
|
||||
}
|
||||
|
||||
private bool CanGenerateTrickplay(Video video, int interval)
|
||||
|
||||
@@ -7,6 +7,7 @@ using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
@@ -49,7 +50,7 @@ internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
var options = virtualFolder.LibraryOptions;
|
||||
if (options?.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
|
||||
if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
PlayCount = dto.GetInt32(4),
|
||||
IsFavorite = dto.GetBoolean(5),
|
||||
PlaybackPositionTicks = dto.GetInt64(6),
|
||||
LastPlayedDate = dto.IsDBNull(7) ? null : ReadDateTimeFromColumn(dto, 7),
|
||||
LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
|
||||
AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
|
||||
SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
|
||||
Likes = null,
|
||||
@@ -524,28 +524,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
};
|
||||
}
|
||||
|
||||
private static DateTime? ReadDateTimeFromColumn(SqliteDataReader reader, int index)
|
||||
{
|
||||
// Try reading as a formatted date string first (handles ISO-8601 dates).
|
||||
if (reader.TryReadDateTime(index, out var dateTimeResult))
|
||||
{
|
||||
return dateTimeResult;
|
||||
}
|
||||
|
||||
// Some databases have Unix epoch timestamps stored as integers.
|
||||
// SqliteDataReader.GetDateTime interprets integers as Julian dates, which crashes
|
||||
// for Unix epoch values. Handle them explicitly.
|
||||
var rawValue = reader.GetValue(index);
|
||||
if (rawValue is long unixTimestamp
|
||||
&& unixTimestamp > 0
|
||||
&& unixTimestamp <= DateTimeOffset.MaxValue.ToUnixTimeSeconds())
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private AncestorId GetAncestorId(SqliteDataReader reader)
|
||||
{
|
||||
return new AncestorId()
|
||||
|
||||
@@ -162,7 +162,7 @@ public sealed class SetupServer : IDisposable
|
||||
{
|
||||
var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6);
|
||||
knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6);
|
||||
var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger<NetworkManager>(), false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
|
||||
var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
|
||||
Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
|
||||
bindInterfaces,
|
||||
config.InternalHttpPort,
|
||||
|
||||
@@ -154,6 +154,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||
return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
|
||||
}
|
||||
|
||||
protected override bool GetBlockUnratedValue(User user)
|
||||
{
|
||||
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Music;
|
||||
|
||||
@@ -1171,18 +1171,11 @@ namespace MediaBrowser.Controller.Entities
|
||||
info.Video3DFormat = video.Video3DFormat;
|
||||
info.Timestamp = video.Timestamp;
|
||||
|
||||
if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
|
||||
if (video.IsShortcut)
|
||||
{
|
||||
var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
|
||||
|
||||
// Only allow remote shortcut paths — local file paths in .strm files
|
||||
// could be used to read arbitrary files from the server.
|
||||
if (shortcutProtocol != MediaProtocol.File)
|
||||
{
|
||||
info.IsRemote = true;
|
||||
info.Path = video.ShortcutPath;
|
||||
info.Protocol = shortcutProtocol;
|
||||
}
|
||||
info.IsRemote = true;
|
||||
info.Path = video.ShortcutPath;
|
||||
info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(info.Container))
|
||||
@@ -1607,6 +1600,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
if (string.IsNullOrEmpty(rating))
|
||||
{
|
||||
Logger.LogDebug("{0} has no parental rating set.", Name);
|
||||
return !GetBlockUnratedValue(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
ExcludePersonTypes = excludePersonTypes;
|
||||
}
|
||||
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of items the query should return.
|
||||
/// </summary>
|
||||
@@ -30,8 +28,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
public IReadOnlyList<string> PersonTypes { get; }
|
||||
|
||||
public IReadOnlyList<string> ExcludePersonTypes { get; }
|
||||
|
||||
@@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
public partial class EncodingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The codec validation regex string.
|
||||
/// The codec validation regex.
|
||||
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||
/// This should matches all common valid codecs.
|
||||
/// </summary>
|
||||
public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||
|
||||
/// <summary>
|
||||
/// The level validation regex string.
|
||||
/// The level validation regex.
|
||||
/// This regular expression matches strings representing a double.
|
||||
/// </summary>
|
||||
public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||
|
||||
private const string _defaultMjpegEncoder = "mjpeg";
|
||||
|
||||
@@ -85,7 +85,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
||||
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
||||
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
|
||||
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
|
||||
|
||||
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
|
||||
|
||||
private static readonly string[] _videoProfilesH264 =
|
||||
[
|
||||
@@ -179,22 +180,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
RemoveHdr10Plus,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The codec validation regex.
|
||||
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||
/// This should matches all common valid codecs.
|
||||
/// </summary>
|
||||
[GeneratedRegex(ContainerValidationRegexStr)]
|
||||
public static partial Regex ContainerValidationRegex();
|
||||
|
||||
/// <summary>
|
||||
/// The level validation regex string.
|
||||
/// This regular expression matches strings representing a double.
|
||||
/// </summary>
|
||||
[GeneratedRegex(LevelValidationRegexStr)]
|
||||
public static partial Regex LevelValidationRegex();
|
||||
|
||||
[GeneratedRegex(@"\s+")]
|
||||
private static partial Regex WhiteSpaceRegex();
|
||||
|
||||
@@ -491,7 +476,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return GetMjpegEncoder(state, encodingOptions);
|
||||
}
|
||||
|
||||
if (ContainerValidationRegex().IsMatch(codec))
|
||||
if (_containerValidationRegex.IsMatch(codec))
|
||||
{
|
||||
return codec.ToLowerInvariant();
|
||||
}
|
||||
@@ -532,7 +517,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public static string GetInputFormat(string container)
|
||||
{
|
||||
if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container))
|
||||
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -750,7 +735,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var codec = state.OutputAudioCodec;
|
||||
|
||||
if (!ContainerValidationRegex().IsMatch(codec))
|
||||
if (!_containerValidationRegex.IsMatch(codec))
|
||||
{
|
||||
codec = "aac";
|
||||
}
|
||||
@@ -1581,15 +1566,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
int bitrate = state.OutputVideoBitrate.Value;
|
||||
|
||||
// Bit rate under 1000k is not allowed in h264_qsv.
|
||||
// Bit rate under 1000k is not allowed in h264_qsv
|
||||
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bitrate = Math.Max(bitrate, 1000);
|
||||
}
|
||||
|
||||
// Currently use the same buffer size for all non-QSV encoders.
|
||||
// Use long arithmetic to prevent int32 overflow for very high bitrate values.
|
||||
int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue);
|
||||
// Currently use the same buffer size for all encoders
|
||||
int bufsize = bitrate * 2;
|
||||
|
||||
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1619,13 +1603,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
|
||||
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
|
||||
// Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow
|
||||
// (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million)
|
||||
int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue);
|
||||
int qsvInitOcc = (int)Math.Min((long)bitrate * 2, int.MaxValue);
|
||||
int qsvBufsize = (int)Math.Min((long)bitrate * 4, int.MaxValue);
|
||||
|
||||
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}");
|
||||
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}");
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -1804,40 +1782,38 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
|
||||
{
|
||||
if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
||||
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
||||
// https://en.wikipedia.org/wiki/AV1#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 15)
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "15";
|
||||
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
||||
// https://en.wikipedia.org/wiki/AV1#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 15)
|
||||
{
|
||||
return "15";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||
if (requestLevel < 0 || requestLevel >= 150)
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "150";
|
||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||
if (requestLevel < 0 || requestLevel >= 150)
|
||||
{
|
||||
return "150";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 51)
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "51";
|
||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 51)
|
||||
{
|
||||
return "51";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2227,10 +2203,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec));
|
||||
var level = state.GetRequestedLevel(targetVideoCodec);
|
||||
|
||||
if (!string.IsNullOrEmpty(level))
|
||||
{
|
||||
level = NormalizeTranscodingLevel(state, level);
|
||||
|
||||
// libx264, QSV, AMF can adjust the given level to match the output.
|
||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -2628,16 +2606,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// Cap the max target bitrate to 400 Mbps.
|
||||
// No consumer or professional hardware transcode target exceeds this value
|
||||
// (Intel QSV tops out at ~300 Mbps for H.264; HEVC High Tier Level 5.x is ~240 Mbps).
|
||||
// Without this cap, plugin-provided MPEG-TS streams with no usable bitrate metadata
|
||||
// can produce unreasonably large -bufsize/-maxrate values for the encoder.
|
||||
// Note: the existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps) only
|
||||
// applies when a LiveStreamId is set (M3U/HDHR sources). Plugin streams and other
|
||||
// sources that bypass the LiveTV pipeline are not covered by it.
|
||||
const int MaxSaneBitrate = 400_000_000; // 400 Mbps
|
||||
return Math.Min(bitrate ?? 0, MaxSaneBitrate);
|
||||
// Cap the max target bitrate to intMax/2 to satisfy the bufsize=bitrate*2.
|
||||
return Math.Min(bitrate ?? 0, int.MaxValue / 2);
|
||||
}
|
||||
|
||||
private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
|
||||
@@ -6403,15 +6373,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
|
||||
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
|
||||
&& ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
|| (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false)))
|
||||
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
||||
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|
||||
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7254,10 +7226,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
||||
}
|
||||
|
||||
int readrate = 0;
|
||||
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
|
||||
{
|
||||
readrate = 1;
|
||||
inputModifier += " -re";
|
||||
}
|
||||
else if (encodingOptions.EnableSegmentDeletion
|
||||
@@ -7268,15 +7238,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
|
||||
// to prevent ffmpeg from exiting prematurely (due to fast drive)
|
||||
readrate = 10;
|
||||
inputModifier += $" -readrate {readrate}";
|
||||
}
|
||||
|
||||
// Set a larger catchup value to revert to the old behavior,
|
||||
// otherwise, remuxing might stall due to this new option
|
||||
if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption)
|
||||
{
|
||||
inputModifier += $" -readrate_catchup {readrate * 100}";
|
||||
inputModifier += " -readrate 10";
|
||||
}
|
||||
|
||||
var flags = new List<string>();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -32,13 +31,4 @@ public interface IMediaSegmentProvider
|
||||
/// <param name="item">The base item to extract segments from.</param>
|
||||
/// <returns>True if item is supported, otherwise false.</returns>
|
||||
ValueTask<bool> Supports(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Called when extracted segment data for an item is being pruned.
|
||||
/// Providers should delete any cached analysis data they hold for the given item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item whose data is being pruned.</param>
|
||||
/// <param name="cancellationToken">Abort token.</param>
|
||||
/// <returns>A task representing the asynchronous cleanup operation.</returns>
|
||||
Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
await ExtractAllAttachmentsInternal(
|
||||
inputFile,
|
||||
mediaSource,
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -122,6 +123,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
private async Task ExtractAllAttachmentsInternal(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
bool isExternal,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
|
||||
@@ -140,19 +142,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
return;
|
||||
}
|
||||
|
||||
// Files without video/audio streams (e.g. MKS subtitle files) don't need a dummy
|
||||
// output since there are no streams to process. Omit "-t 0 -f null null" so ffmpeg
|
||||
// doesn't fail trying to open an output with no streams. It will exit with code 1
|
||||
// ("at least one output file must be specified") which is expected and harmless
|
||||
// since we only need the -dump_attachment side effect.
|
||||
var hasVideoOrAudioStream = mediaSource.MediaStreams
|
||||
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:t \"\" -y {0} -i {1} {2}",
|
||||
"-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
|
||||
inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
|
||||
inputPath,
|
||||
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
|
||||
inputPath);
|
||||
|
||||
int exitCode;
|
||||
|
||||
@@ -191,7 +185,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (hasVideoOrAudioStream || exitCode != 1)
|
||||
if (isExternal && exitCode == 1)
|
||||
{
|
||||
// ffmpeg returns exitCode 1 because there is no video or audio stream
|
||||
// this can be ignored
|
||||
}
|
||||
else
|
||||
{
|
||||
failed = true;
|
||||
|
||||
@@ -206,8 +205,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed && !Directory.Exists(outputFolder))
|
||||
else if (!Directory.Exists(outputFolder))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
@@ -248,7 +246,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
await ExtractAttachmentInternal(
|
||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||
mediaSource,
|
||||
mediaAttachment.Index,
|
||||
attachmentPath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -260,7 +257,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
private async Task ExtractAttachmentInternal(
|
||||
string inputPath,
|
||||
MediaSourceInfo mediaSource,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -271,15 +267,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||
|
||||
var hasVideoOrAudioStream = mediaSource.MediaStreams
|
||||
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{1} \"{2}\" -i {0} {3}",
|
||||
"-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
EncodingUtils.NormalizePath(outputPath),
|
||||
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
|
||||
EncodingUtils.NormalizePath(outputPath));
|
||||
|
||||
int exitCode;
|
||||
|
||||
@@ -317,26 +310,22 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (hasVideoOrAudioStream || exitCode != 1)
|
||||
{
|
||||
failed = true;
|
||||
failed = true;
|
||||
|
||||
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
|
||||
try
|
||||
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
|
||||
try
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed && !File.Exists(outputPath))
|
||||
else if (!File.Exists(outputPath))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <param name="dict">The dict.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string?> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string?> dict)
|
||||
private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict)
|
||||
{
|
||||
return new Dictionary<string, string?>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -20,21 +22,21 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
[JsonPropertyName("profile")]
|
||||
public string? Profile { get; set; }
|
||||
public string Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_name.</value>
|
||||
[JsonPropertyName("codec_name")]
|
||||
public string? CodecName { get; set; }
|
||||
public string CodecName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_long_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_long_name.</value>
|
||||
[JsonPropertyName("codec_long_name")]
|
||||
public string? CodecLongName { get; set; }
|
||||
public string CodecLongName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_type.
|
||||
@@ -48,49 +50,49 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <value>The sample_rate.</value>
|
||||
[JsonPropertyName("sample_rate")]
|
||||
public string? SampleRate { get; set; }
|
||||
public string SampleRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channels.
|
||||
/// </summary>
|
||||
/// <value>The channels.</value>
|
||||
[JsonPropertyName("channels")]
|
||||
public int? Channels { get; set; }
|
||||
public int Channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel_layout.
|
||||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
[JsonPropertyName("channel_layout")]
|
||||
public string? ChannelLayout { get; set; }
|
||||
public string ChannelLayout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The avg_frame_rate.</value>
|
||||
[JsonPropertyName("avg_frame_rate")]
|
||||
public string? AverageFrameRate { get; set; }
|
||||
public string AverageFrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[JsonPropertyName("duration")]
|
||||
public string? Duration { get; set; }
|
||||
public string Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[JsonPropertyName("bit_rate")]
|
||||
public string? BitRate { get; set; }
|
||||
public string BitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
[JsonPropertyName("width")]
|
||||
public int? Width { get; set; }
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refs.
|
||||
@@ -104,21 +106,21 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
[JsonPropertyName("height")]
|
||||
public int? Height { get; set; }
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The display_aspect_ratio.</value>
|
||||
[JsonPropertyName("display_aspect_ratio")]
|
||||
public string? DisplayAspectRatio { get; set; }
|
||||
public string DisplayAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string?>? Tags { get; set; }
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_sample.
|
||||
@@ -139,7 +141,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <value>The r_frame_rate.</value>
|
||||
[JsonPropertyName("r_frame_rate")]
|
||||
public string? RFrameRate { get; set; }
|
||||
public string RFrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the has_b_frames.
|
||||
@@ -153,70 +155,70 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <value>The sample_aspect_ratio.</value>
|
||||
[JsonPropertyName("sample_aspect_ratio")]
|
||||
public string? SampleAspectRatio { get; set; }
|
||||
public string SampleAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pix_fmt.
|
||||
/// </summary>
|
||||
/// <value>The pix_fmt.</value>
|
||||
[JsonPropertyName("pix_fmt")]
|
||||
public string? PixelFormat { get; set; }
|
||||
public string PixelFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
[JsonPropertyName("level")]
|
||||
public int? Level { get; set; }
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time_base.
|
||||
/// </summary>
|
||||
/// <value>The time_base.</value>
|
||||
[JsonPropertyName("time_base")]
|
||||
public string? TimeBase { get; set; }
|
||||
public string TimeBase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[JsonPropertyName("start_time")]
|
||||
public string? StartTime { get; set; }
|
||||
public string StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_time_base.
|
||||
/// </summary>
|
||||
/// <value>The codec_time_base.</value>
|
||||
[JsonPropertyName("codec_time_base")]
|
||||
public string? CodecTimeBase { get; set; }
|
||||
public string CodecTimeBase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag.</value>
|
||||
[JsonPropertyName("codec_tag")]
|
||||
public string? CodecTag { get; set; }
|
||||
public string CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag_string?.
|
||||
/// Gets or sets the codec_tag_string.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag_string?.</value>
|
||||
[JsonPropertyName("codec_tag_string?")]
|
||||
public string? CodecTagString { get; set; }
|
||||
/// <value>The codec_tag_string.</value>
|
||||
[JsonPropertyName("codec_tag_string")]
|
||||
public string CodecTagString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_fmt.
|
||||
/// </summary>
|
||||
/// <value>The sample_fmt.</value>
|
||||
[JsonPropertyName("sample_fmt")]
|
||||
public string? SampleFmt { get; set; }
|
||||
public string SampleFmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dmix_mode.
|
||||
/// </summary>
|
||||
/// <value>The dmix_mode.</value>
|
||||
[JsonPropertyName("dmix_mode")]
|
||||
public string? DmixMode { get; set; }
|
||||
public string DmixMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_pts.
|
||||
@@ -230,90 +232,90 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
[JsonPropertyName("is_avc")]
|
||||
public bool? IsAvc { get; set; }
|
||||
public bool IsAvc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nal_length_size.
|
||||
/// </summary>
|
||||
/// <value>The nal_length_size.</value>
|
||||
[JsonPropertyName("nal_length_size")]
|
||||
public string? NalLengthSize { get; set; }
|
||||
public string NalLengthSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_cmixlev.</value>
|
||||
[JsonPropertyName("ltrt_cmixlev")]
|
||||
public string? LtrtCmixlev { get; set; }
|
||||
public string LtrtCmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_surmixlev.</value>
|
||||
[JsonPropertyName("ltrt_surmixlev")]
|
||||
public string? LtrtSurmixlev { get; set; }
|
||||
public string LtrtSurmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_cmixlev.</value>
|
||||
[JsonPropertyName("loro_cmixlev")]
|
||||
public string? LoroCmixlev { get; set; }
|
||||
public string LoroCmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_surmixlev.</value>
|
||||
[JsonPropertyName("loro_surmixlev")]
|
||||
public string? LoroSurmixlev { get; set; }
|
||||
public string LoroSurmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the field_order.
|
||||
/// </summary>
|
||||
/// <value>The field_order.</value>
|
||||
[JsonPropertyName("field_order")]
|
||||
public string? FieldOrder { get; set; }
|
||||
public string FieldOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
/// <value>The disposition.</value>
|
||||
[JsonPropertyName("disposition")]
|
||||
public IReadOnlyDictionary<string, int>? Disposition { get; set; }
|
||||
public IReadOnlyDictionary<string, int> Disposition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color range.
|
||||
/// </summary>
|
||||
/// <value>The color range.</value>
|
||||
[JsonPropertyName("color_range")]
|
||||
public string? ColorRange { get; set; }
|
||||
public string ColorRange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color space.
|
||||
/// </summary>
|
||||
/// <value>The color space.</value>
|
||||
[JsonPropertyName("color_space")]
|
||||
public string? ColorSpace { get; set; }
|
||||
public string ColorSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color transfer.
|
||||
/// </summary>
|
||||
/// <value>The color transfer.</value>
|
||||
[JsonPropertyName("color_transfer")]
|
||||
public string? ColorTransfer { get; set; }
|
||||
public string ColorTransfer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color primaries.
|
||||
/// </summary>
|
||||
/// <value>The color primaries.</value>
|
||||
[JsonPropertyName("color_primaries")]
|
||||
public string? ColorPrimaries { get; set; }
|
||||
public string ColorPrimaries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the side_data_list.
|
||||
/// </summary>
|
||||
/// <value>The side_data_list.</value>
|
||||
[JsonPropertyName("side_data_list")]
|
||||
public IReadOnlyList<MediaStreamInfoSideData>? SideDataList { get; set; }
|
||||
public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,18 +697,24 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// <returns>MediaStream.</returns>
|
||||
private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> frameInfoList)
|
||||
{
|
||||
// These are mp4 chapters
|
||||
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Edit: but these are also sometimes subtitles?
|
||||
// return null;
|
||||
}
|
||||
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.CodecName,
|
||||
Profile = streamInfo.Profile,
|
||||
Width = streamInfo.Width,
|
||||
Height = streamInfo.Height,
|
||||
Level = streamInfo.Level,
|
||||
Index = streamInfo.Index,
|
||||
PixelFormat = streamInfo.PixelFormat,
|
||||
NalLengthSize = streamInfo.NalLengthSize,
|
||||
TimeBase = streamInfo.TimeBase,
|
||||
CodecTimeBase = streamInfo.CodecTimeBase
|
||||
CodecTimeBase = streamInfo.CodecTimeBase,
|
||||
IsAVC = streamInfo.IsAvc
|
||||
};
|
||||
|
||||
// Filter out junk
|
||||
@@ -768,6 +774,10 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
stream.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
||||
|
||||
// Graphical subtitle may have width and height info
|
||||
stream.Width = streamInfo.Width;
|
||||
stream.Height = streamInfo.Height;
|
||||
|
||||
if (string.IsNullOrEmpty(stream.Title))
|
||||
{
|
||||
// mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
|
||||
@@ -780,7 +790,6 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
else if (streamInfo.CodecType == CodecType.Video)
|
||||
{
|
||||
stream.IsAVC = streamInfo.IsAvc;
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
|
||||
|
||||
@@ -813,6 +822,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
stream.Type = MediaStreamType.Video;
|
||||
}
|
||||
|
||||
stream.Width = streamInfo.Width;
|
||||
stream.Height = streamInfo.Height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
if (streamInfo.BitsPerSample > 0)
|
||||
@@ -1080,8 +1091,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
&& width > 0
|
||||
&& height > 0))
|
||||
{
|
||||
width = info.Width.Value;
|
||||
height = info.Height.Value;
|
||||
width = info.Width;
|
||||
height = info.Height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
|
||||
@@ -577,7 +577,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0}",
|
||||
"-i {0} -copyts",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
@@ -602,7 +602,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
outputPaths.Add(outputPath);
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
|
||||
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||
streamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
@@ -621,7 +621,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0}",
|
||||
"-i {0} -copyts",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
@@ -647,7 +647,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
outputPaths.Add(outputPath);
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
|
||||
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||
streamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
|
||||
@@ -789,12 +789,6 @@ namespace MediaBrowser.Model.Dto
|
||||
/// <value>The gain required for audio normalization.</value>
|
||||
public float? NormalizationGain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gain required for audio normalization. This field is inherited from music album normalization gain.
|
||||
/// </summary>
|
||||
/// <value>The gain required for audio normalization.</value>
|
||||
public float? AlbumNormalizationGain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current program.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Books.Isbn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class IsbnExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "ISBN";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "ISBN";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Books.Isbn;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class IsbnExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "ISBN";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId("ISBN", out var externalId))
|
||||
{
|
||||
if (item is Book)
|
||||
{
|
||||
yield return $"https://search.worldcat.org/search?q=bn:{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,28 +262,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
private void FetchShortcutInfo(BaseItem item)
|
||||
{
|
||||
var shortcutPath = File.ReadAllLines(item.Path)
|
||||
item.ShortcutPath = File.ReadAllLines(item.Path)
|
||||
.Select(NormalizeStrmLine)
|
||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(shortcutPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow remote URLs in .strm files to prevent local file access
|
||||
if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri)
|
||||
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
item.ShortcutPath = shortcutPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.ComicVine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ComicVineExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "Comic Vine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "ComicVine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.ComicVine;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class ComicVineExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Comic Vine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId("ComicVine", out var externalId))
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case Person:
|
||||
case Book:
|
||||
yield return $"https://comicvine.gamespot.com/{externalId}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.ComicVine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ComicVinePersonExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "Comic Vine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "ComicVine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Person;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.GoogleBooks
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class GoogleBooksExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "Google Books";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "GoogleBooks";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.GoogleBooks;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class GoogleBooksExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "Google Books";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId("GoogleBooks", out var externalId))
|
||||
{
|
||||
if (item is Book)
|
||||
{
|
||||
yield return $"https://books.google.com/books?id={externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -33,7 +32,6 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
private readonly ILibraryMonitor _monitor;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly HashSet<string> _allowedSubtitleFormats;
|
||||
|
||||
private readonly ISubtitleProvider[] _subtitleProviders;
|
||||
|
||||
@@ -43,8 +41,7 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
ILibraryMonitor monitor,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
ILocalizationManager localizationManager,
|
||||
IEnumerable<ISubtitleProvider> subtitleProviders,
|
||||
NamingOptions namingOptions)
|
||||
IEnumerable<ISubtitleProvider> subtitleProviders)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
@@ -54,9 +51,6 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
_subtitleProviders = subtitleProviders
|
||||
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
|
||||
.ToArray();
|
||||
_allowedSubtitleFormats = new HashSet<string>(
|
||||
namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -177,12 +171,6 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
/// <inheritdoc />
|
||||
public Task UploadSubtitle(Video video, SubtitleResponse response)
|
||||
{
|
||||
var format = response.Format;
|
||||
if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format))
|
||||
{
|
||||
throw new ArgumentException($"Unsupported subtitle format: '{format}'");
|
||||
}
|
||||
|
||||
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
|
||||
return TrySaveSubtitle(video, libraryOptions, response);
|
||||
}
|
||||
@@ -205,13 +193,7 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
}
|
||||
|
||||
var savePaths = new List<string>();
|
||||
var language = response.Language.ToLowerInvariant();
|
||||
if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Language contains invalid characters.");
|
||||
}
|
||||
|
||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language;
|
||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
|
||||
|
||||
if (response.IsForced)
|
||||
{
|
||||
@@ -239,22 +221,15 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
|
||||
private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
|
||||
{
|
||||
if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException($"Invalid subtitle format: {extension}");
|
||||
}
|
||||
|
||||
List<Exception>? exs = null;
|
||||
|
||||
foreach (var savePath in savePaths)
|
||||
{
|
||||
var path = Path.GetFullPath(savePath + "." + extension);
|
||||
var path = savePath + "." + extension;
|
||||
try
|
||||
{
|
||||
var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar;
|
||||
var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar;
|
||||
if (path.StartsWith(containingFolder, StringComparison.Ordinal)
|
||||
|| path.StartsWith(metadataFolder, StringComparison.Ordinal))
|
||||
if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)
|
||||
|| path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
|
||||
{
|
||||
var fileExists = File.Exists(path);
|
||||
var counter = 0;
|
||||
|
||||
@@ -547,7 +547,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
|
||||
}
|
||||
|
||||
if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
|
||||
{
|
||||
writer.WriteElementString("collectionnumber", tmdbCollection);
|
||||
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
|
||||
|
||||
@@ -24,29 +24,61 @@ public class SkiaEncoder : IImageEncoder
|
||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
private readonly ILogger<SkiaEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private static readonly SKTypeface?[] _typefaces = InitializeTypefaces();
|
||||
private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||
new SKSizeI(3, 3),
|
||||
[
|
||||
0, -.1f, 0,
|
||||
-.1f, 1.4f, -.1f,
|
||||
0, -.1f, 0
|
||||
],
|
||||
1f,
|
||||
0f,
|
||||
new SKPointI(1, 1),
|
||||
SKShaderTileMode.Clamp,
|
||||
true);
|
||||
private static readonly SKImageFilter _imageFilter;
|
||||
private static readonly SKTypeface[] _typefaces;
|
||||
|
||||
/// <summary>
|
||||
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
|
||||
/// </summary>
|
||||
public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||
public static readonly SKSamplingOptions UpscaleSamplingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
|
||||
/// </summary>
|
||||
public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
|
||||
public static readonly SKSamplingOptions DefaultSamplingOptions;
|
||||
|
||||
#pragma warning disable CA1810
|
||||
static SkiaEncoder()
|
||||
#pragma warning restore CA1810
|
||||
{
|
||||
var kernel = new[]
|
||||
{
|
||||
0, -.1f, 0,
|
||||
-.1f, 1.4f, -.1f,
|
||||
0, -.1f, 0,
|
||||
};
|
||||
|
||||
var kernelSize = new SKSizeI(3, 3);
|
||||
var kernelOffset = new SKPointI(1, 1);
|
||||
_imageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||
kernelSize,
|
||||
kernel,
|
||||
1f,
|
||||
0f,
|
||||
kernelOffset,
|
||||
SKShaderTileMode.Clamp,
|
||||
true);
|
||||
|
||||
// Initialize the list of typefaces
|
||||
// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
|
||||
// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F)
|
||||
_typefaces =
|
||||
[
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
|
||||
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
|
||||
];
|
||||
|
||||
// use cubic for upscaling
|
||||
UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||
// use bilinear for everything else
|
||||
DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
|
||||
@@ -100,7 +132,7 @@ public class SkiaEncoder : IImageEncoder
|
||||
/// <summary>
|
||||
/// Gets the default typeface to use.
|
||||
/// </summary>
|
||||
public static SKTypeface? DefaultTypeFace => _typefaces.Last();
|
||||
public static SKTypeface DefaultTypeFace => _typefaces.Last();
|
||||
|
||||
/// <summary>
|
||||
/// Check if the native lib is available.
|
||||
@@ -120,40 +152,6 @@ public class SkiaEncoder : IImageEncoder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the list of typefaces
|
||||
/// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
|
||||
/// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F).
|
||||
/// </summary>
|
||||
/// <returns>The list of typefaces.</returns>
|
||||
private static SKTypeface?[] InitializeTypefaces()
|
||||
{
|
||||
int[] chars = [
|
||||
'鸡', // CJK Simplified Chinese
|
||||
'雞', // CJK Traditional Chinese
|
||||
'ノ', // CJK Japanese
|
||||
'각', // CJK Korean
|
||||
128169, // Emojis, 128169 is the Pile of Poo (💩) emoji
|
||||
'ז', // Hebrew
|
||||
'ي' // Arabic
|
||||
];
|
||||
var fonts = new List<SKTypeface>(chars.Length + 1);
|
||||
foreach (var ch in chars)
|
||||
{
|
||||
var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch);
|
||||
if (font is not null)
|
||||
{
|
||||
fonts.Add(font);
|
||||
}
|
||||
}
|
||||
|
||||
// Default font
|
||||
fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
|
||||
?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a'));
|
||||
|
||||
return fonts.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
|
||||
/// </summary>
|
||||
@@ -811,7 +809,7 @@ public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
foreach (var typeface in _typefaces)
|
||||
{
|
||||
if (typeface is not null && typeface.ContainsGlyphs(c))
|
||||
if (typeface.ContainsGlyphs(c))
|
||||
{
|
||||
return typeface;
|
||||
}
|
||||
|
||||
@@ -156,13 +156,6 @@ namespace Jellyfin.LiveTv.IO
|
||||
if (mediaSource.ReadAtNativeFramerate)
|
||||
{
|
||||
inputModifier += " -re";
|
||||
|
||||
// Set a larger catchup value to revert to the old behavior,
|
||||
// otherwise, remuxing might stall due to this new option
|
||||
if (_mediaEncoder.EncoderVersion >= new Version(8, 0))
|
||||
{
|
||||
inputModifier += " -readrate_catchup 100";
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaSource.RequiresLooping)
|
||||
|
||||
@@ -93,13 +93,6 @@ namespace Jellyfin.LiveTv.TunerHosts
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
||||
{
|
||||
if (!IsValidChannelUrl(trimmedLine))
|
||||
{
|
||||
_logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine);
|
||||
extInf = string.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
|
||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -254,16 +247,6 @@ namespace Jellyfin.LiveTv.TunerHosts
|
||||
return numberString;
|
||||
}
|
||||
|
||||
private static bool IsValidChannelUrl(string url)
|
||||
{
|
||||
return Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsValidChannelNumber(string numberString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(numberString)
|
||||
|
||||
@@ -747,13 +747,12 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
|
||||
{
|
||||
return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
|
||||
return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to use for messages.</param>
|
||||
/// <param name="individualInterfaces">Defines that only known interfaces should be used.</param>
|
||||
/// <param name="configurationManager">The ConfigurationManager.</param>
|
||||
/// <param name="knownInterfaces">The known interfaces that gets returned if possible or instructed.</param>
|
||||
@@ -761,7 +760,6 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
/// <param name="readIpv6">Include IPV6 type interfaces.</param>
|
||||
/// <returns>A list of ip address of which jellyfin should bind to.</returns>
|
||||
public static IReadOnlyList<IPData> GetAllBindInterfaces(
|
||||
ILogger<NetworkManager> logger,
|
||||
bool individualInterfaces,
|
||||
IConfigurationManager configurationManager,
|
||||
IReadOnlyList<IPData> knownInterfaces,
|
||||
@@ -775,13 +773,6 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
return knownInterfaces;
|
||||
}
|
||||
|
||||
// TODO: remove when upgrade to dotnet 11 is done
|
||||
if (readIpv6 && !Socket.OSSupportsIPv6)
|
||||
{
|
||||
logger.LogWarning("IPv6 Unsupported by OS, not listening on IPv6");
|
||||
readIpv6 = false;
|
||||
}
|
||||
|
||||
// No bind address and no exclusions, so listen on all interfaces.
|
||||
var result = new List<IPData>();
|
||||
if (readIpv4 && readIpv6)
|
||||
@@ -878,20 +869,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
if (availableInterfaces.Count == 0)
|
||||
{
|
||||
// There isn't any others, so we'll use the loopback.
|
||||
// Prefer loopback address matching the source's address family
|
||||
if (source is not null && source.AddressFamily == AddressFamily.InterNetwork && IsIPv4Enabled)
|
||||
{
|
||||
result = "127.0.0.1";
|
||||
}
|
||||
else if (source is not null && source.AddressFamily == AddressFamily.InterNetworkV6 && IsIPv6Enabled)
|
||||
{
|
||||
result = "::1";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = IsIPv4Enabled ? "127.0.0.1" : "::1";
|
||||
}
|
||||
|
||||
result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
|
||||
_logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
|
||||
return result;
|
||||
}
|
||||
@@ -916,19 +894,9 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to an interface matching the source's address family, or first available
|
||||
var preferredInterface = availableInterfaces
|
||||
.FirstOrDefault(x => x.Address.AddressFamily == source.AddressFamily);
|
||||
|
||||
if (preferredInterface is not null)
|
||||
{
|
||||
result = NetworkUtils.FormatIPString(preferredInterface.Address);
|
||||
_logger.LogDebug("{Source}: No matching subnet found, using interface with matching address family: {Result}", source, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback to first available interface
|
||||
result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
|
||||
_logger.LogDebug("{Source}: No matching interfaces found, using first available interface as bind address: {Result}", source, result);
|
||||
_logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,8 +209,8 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
||||
Assert.Equal("mkv,webm", res.Container);
|
||||
|
||||
Assert.Equal(2, res.MediaStreams.Count);
|
||||
Assert.Equal(540, res.MediaStreams[0].Width);
|
||||
Assert.Equal(360, res.MediaStreams[0].Height);
|
||||
|
||||
Assert.False(res.MediaStreams[0].IsAVC);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -29,7 +29,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[InlineData("[OCN] 애타는 로맨스 720p-NEXT", "애타는 로맨스")]
|
||||
[InlineData("[tvN] 혼술남녀.E01-E16.720p-NEXT", "혼술남녀")]
|
||||
[InlineData("[tvN] 연애말고 결혼 E01~E16 END HDTV.H264.720p-WITH", "연애말고 결혼")]
|
||||
[InlineData("2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE[字].mp4", "2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE")]
|
||||
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
|
||||
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
|
||||
{
|
||||
@@ -45,7 +44,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[InlineData("American.Psycho.mkv")]
|
||||
[InlineData("American Psycho.mkv")]
|
||||
[InlineData("Run lola run (lola rennt) (2009).mp4")]
|
||||
[InlineData("2026年01月05日00時55分00秒-[新]違国日記【ANiMiDNiGHT!!!】#1.mp4")]
|
||||
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
|
||||
{
|
||||
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
@@ -270,13 +269,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(6, result[0].AlternateVersions.Count);
|
||||
|
||||
// Verify 3D recognition is preserved on alternate versions
|
||||
var hsbs = result[0].AlternateVersions.First(v => v.Path.Contains("3d-hsbs", StringComparison.Ordinal));
|
||||
Assert.True(hsbs.Is3D);
|
||||
Assert.Equal("hsbs", hsbs.Format3D);
|
||||
Assert.Equal(7, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -441,39 +435,5 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GivenUnderscoreSeparator_GroupsVersions()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie (2020)/Movie (2020)_4K.mkv",
|
||||
"/movies/Movie (2020)/Movie (2020)_1080p.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GivenDotSeparator_GroupsVersions()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie (2020)/Movie (2020).UHD.mkv",
|
||||
"/movies/Movie (2020)/Movie (2020).1080p.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,8 +377,6 @@ namespace Jellyfin.Networking.Tests
|
||||
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "10.0.0.1", "192.168.1.209", "10.0.0.1")] // LAN not bound, so return external.
|
||||
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "8.8.8.8", "10.0.0.1")] // return external bind address
|
||||
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "192.168.1.210", "192.168.1.208")] // return LAN bind address
|
||||
// Cross-subnet IPv4 request should return IPv4, not IPv6 (Issue #15898)
|
||||
[InlineData("192.168.1.208/24,-16,eth16|fd00::1/64,10,eth7", "192.168.1.0/24", "", "192.168.2.100", "192.168.1.208")]
|
||||
public void GetBindInterface_ValidSourceGiven_Success(string interfaces, string lan, string bind, string source, string result)
|
||||
{
|
||||
var conf = new NetworkConfiguration
|
||||
|
||||
Reference in New Issue
Block a user