Compare commits
157 Commits
master
...
feature/cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f88cab9ed2 | ||
|
|
0da3d7e29a | ||
|
|
f63a22e65e | ||
|
|
1325603fd0 | ||
|
|
26668426d4 | ||
|
|
945cb3a116 | ||
|
|
d12a39e0c0 | ||
|
|
b9b7b6f7ef | ||
|
|
dd73b1d309 | ||
|
|
464c02885e | ||
|
|
fb4b87df07 | ||
|
|
7574c7eb83 | ||
|
|
d725f9a583 | ||
|
|
cddaff96c2 | ||
|
|
6fbc0f87df | ||
|
|
0bc8b92b6a | ||
|
|
10662e75e4 | ||
|
|
a2b1936e73 | ||
|
|
2df546af6d | ||
|
|
338b480217 | ||
|
|
2943bb6fdd | ||
|
|
94edcbd2d1 | ||
|
|
a8d1cdefac | ||
|
|
a518160a6f | ||
|
|
b56de6493f | ||
|
|
093cfc3f3b | ||
|
|
49775b1f6a | ||
|
|
22d593b8e9 | ||
|
|
2cb7fb52d2 | ||
|
|
8433b6d8a4 | ||
|
|
32d2414de0 | ||
|
|
317a3a47c3 | ||
|
|
7cc33291f5 | ||
|
|
28854ebf2e | ||
|
|
7500f4a37f | ||
|
|
9ae454aa18 | ||
|
|
49beabce9e | ||
|
|
845b8cdc8f | ||
|
|
c86f6439c5 | ||
|
|
559e0088e5 | ||
|
|
adaca95590 | ||
|
|
09a1c31fa3 | ||
|
|
e4b82025b8 | ||
|
|
78e3702cb0 | ||
|
|
01b20d3b75 | ||
|
|
156761405e | ||
|
|
1805f2259f | ||
|
|
4c587776d6 | ||
|
|
8379b4634a | ||
|
|
9470439cfa | ||
|
|
18096e48e0 | ||
|
|
f2d0ac7b28 | ||
|
|
2ccf08f547 | ||
|
|
1e27f460fe | ||
|
|
4cdd8c8233 | ||
|
|
6e60634c9f | ||
|
|
12c5d6b636 | ||
|
|
b617c62f8e | ||
|
|
035b5895b0 | ||
|
|
22da5187c8 | ||
|
|
5804d6840c | ||
|
|
b50ce1ad6b | ||
|
|
481ee03f35 | ||
|
|
d91adb5d54 | ||
|
|
ef7f138a4e | ||
|
|
2e8d9a311b | ||
|
|
4c5a3fbff3 | ||
|
|
636908fc4d | ||
|
|
997362fc97 | ||
|
|
c5147341e3 | ||
|
|
ca33bcebf0 | ||
|
|
d32f487e8e | ||
|
|
fb65f8f853 | ||
|
|
2a0b90e385 | ||
|
|
dde70fd8a2 | ||
|
|
98d1d0cb35 | ||
|
|
ba76a8f3ad | ||
|
|
8cd5652157 | ||
|
|
8aff4227d9 | ||
|
|
026f7472cb | ||
|
|
daca285568 | ||
|
|
fbb9a0b2c7 | ||
|
|
29b3aa8543 | ||
|
|
94f3725208 | ||
|
|
0ee81e87be | ||
|
|
c491a918c2 | ||
|
|
1e7e46cb82 | ||
|
|
5ae444d96d | ||
|
|
ee7ad83427 | ||
|
|
921d7d3364 | ||
|
|
f8e012582a | ||
|
|
def5956cd1 | ||
|
|
abfbaca336 | ||
|
|
6566188e45 | ||
|
|
078f9584ed | ||
|
|
ee34c75386 | ||
|
|
e8150428b6 | ||
|
|
4b38e35bbb | ||
|
|
435bb14bb2 | ||
|
|
2e5ced5098 | ||
|
|
f4a846aa4d | ||
|
|
7c1063177f | ||
|
|
5878b1ffc5 | ||
|
|
3c3c2aee0d | ||
|
|
511223aac4 | ||
|
|
3b2d64995a | ||
|
|
13c4517a66 | ||
|
|
177b6464ca | ||
|
|
5a9a8363f4 | ||
|
|
49efd68fc7 | ||
|
|
90a8a26c6e | ||
|
|
002c83e6f5 | ||
|
|
7222910b05 | ||
|
|
097cb87f6f | ||
|
|
91c3b1617e | ||
|
|
8f71922734 | ||
|
|
d140630208 | ||
|
|
63a3e55297 | ||
|
|
c2e5081d64 | ||
|
|
4187c6f620 | ||
|
|
e7dbb3afec | ||
|
|
f994dd6211 | ||
|
|
da254ee968 | ||
|
|
4ad3141875 | ||
|
|
b5f0199a25 | ||
|
|
6bf88c049e | ||
|
|
40a33da2a5 | ||
|
|
3596fc0693 | ||
|
|
93824dad97 | ||
|
|
e5656af1f2 | ||
|
|
c127c10458 | ||
|
|
7d1824ea27 | ||
|
|
2966d27c97 | ||
|
|
618ec4543e | ||
|
|
0e4031ae52 | ||
|
|
442af96ed9 | ||
|
|
a305204cfa | ||
|
|
75f472e6a7 | ||
|
|
cc32e8f7cb | ||
|
|
14b3085ff1 | ||
|
|
5691eee4f1 | ||
|
|
1520a697ad | ||
|
|
81b8b0ca4a | ||
|
|
ac3fa3c376 | ||
|
|
7a1c1cd342 | ||
|
|
70c32a26fa | ||
|
|
2b94bb54aa | ||
|
|
0a6e8146be | ||
|
|
305b0fdca3 | ||
|
|
d738386fe2 | ||
|
|
ca830d5be7 | ||
|
|
a5bc4524d8 | ||
|
|
175ee12bbc | ||
|
|
a725220c21 | ||
|
|
a245605152 | ||
|
|
f4a53209f4 | ||
|
|
877251bcae |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.10",
|
||||
"version": "9.0.11",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,11 +1,4 @@
|
||||
# Joshua must review all changes to bump_version and any files it touches
|
||||
bump_version @joshuaboniface
|
||||
.github/ISSUE_TEMPLATE @joshuaboniface
|
||||
MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface
|
||||
Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface
|
||||
MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface
|
||||
MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface
|
||||
Emby.Naming/Emby.Naming.csproj @joshuaboniface
|
||||
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface
|
||||
# Core must approve all changes within the repo config
|
||||
.github/ @jellyfin/core
|
||||
# Joshua must review all changes to deployment and build.sh
|
||||
.ci/* @joshuaboniface
|
||||
deployment/* @joshuaboniface
|
||||
build.sh @joshuaboniface
|
||||
|
||||
10
.github/workflows/ci-codeql-analysis.yml
vendored
10
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -20,18 +20,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
|
||||
8
.github/workflows/ci-compat.yml
vendored
8
.github/workflows/ci-compat.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
@@ -40,14 +40,14 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
|
||||
12
.github/workflows/ci-openapi.yml
vendored
12
.github/workflows/ci-openapi.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
- name: Generate openapi.json
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
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@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
- name: Generate openapi.json
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
strip_components: 1
|
||||
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||
- name: Move openapi.json (unstable) into place
|
||||
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
|
||||
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
strip_components: 1
|
||||
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||
- name: Move openapi.json (stable) into place
|
||||
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
|
||||
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
|
||||
6
.github/workflows/ci-tests.yml
vendored
6
.github/workflows/ci-tests.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@f3c6b3f8a29686284ef7a7cf6dccb79a01d98444 # v5.4.18
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -40,11 +40,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
2
.github/workflows/issue-stale.yml
vendored
2
.github/workflows/issue-stale.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
||||
4
.github/workflows/issue-template-check.yml
vendored
4
.github/workflows/issue-template-check.yml
vendored
@@ -10,11 +10,11 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
2
.github/workflows/pull-request-stale.yaml
vendored
2
.github/workflows/pull-request-stale.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
||||
4
.github/workflows/release-bump-version.yaml
vendored
4
.github/workflows/release-bump-version.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
- [sachk](https://github.com/sachk)
|
||||
- [sammyrc34](https://github.com/sammyrc34)
|
||||
- [samuel9554](https://github.com/samuel9554)
|
||||
- [SapientGuardian](https://github.com/SapientGuardian)
|
||||
- [scheidleon](https://github.com/scheidleon)
|
||||
- [sebPomme](https://github.com/sebPomme)
|
||||
- [SegiH](https://github.com/SegiH)
|
||||
@@ -205,6 +206,8 @@
|
||||
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||
- [TokerX](https://github.com/TokerX)
|
||||
- [GeneMarks](https://github.com/GeneMarks)
|
||||
- [martenumberto](https://github.com/martenumberto)
|
||||
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
@@ -17,7 +17,7 @@
|
||||
<PackageVersion Include="Diacritics" Version="4.0.17" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="3.3.1" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||
@@ -26,33 +26,33 @@
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -62,13 +62,13 @@
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.6.4" />
|
||||
<PackageVersion Include="Polly" Version="8.6.5" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
@@ -84,11 +84,11 @@
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.6.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.9.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
@@ -96,4 +96,4 @@
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -10,12 +10,17 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
public static partial class SeasonPathParser
|
||||
{
|
||||
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
|
||||
|
||||
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPre();
|
||||
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPost();
|
||||
|
||||
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
|
||||
private static partial Regex SeasonPrefix();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse season number from path.
|
||||
/// </summary>
|
||||
@@ -56,44 +61,34 @@ namespace Emby.Naming.TV
|
||||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
{
|
||||
string filename = Path.GetFileName(path);
|
||||
filename = Regex.Replace(filename, "[ ._-]", string.Empty);
|
||||
var fileName = Path.GetFileName(path);
|
||||
|
||||
var seasonPrefixMatch = SeasonPrefix().Match(fileName);
|
||||
if (seasonPrefixMatch.Success &&
|
||||
int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return (val, true);
|
||||
}
|
||||
|
||||
string filename = CleanNameRegex.Replace(fileName, string.Empty);
|
||||
|
||||
if (parentFolderName is not null)
|
||||
{
|
||||
parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
|
||||
filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
|
||||
filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (supportSpecialAliases)
|
||||
if (supportSpecialAliases &&
|
||||
(filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
|
||||
filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (0, true);
|
||||
}
|
||||
|
||||
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (0, true);
|
||||
}
|
||||
return (0, true);
|
||||
}
|
||||
|
||||
if (supportNumericSeasonFolders)
|
||||
if (supportNumericSeasonFolders &&
|
||||
int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
|
||||
{
|
||||
if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return (val, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's'))
|
||||
{
|
||||
var testFilename = filename.AsSpan()[1..];
|
||||
|
||||
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return (val, true);
|
||||
}
|
||||
return (val, true);
|
||||
}
|
||||
|
||||
var preMatch = ProcessPre().Match(filename);
|
||||
@@ -113,8 +108,10 @@ namespace Emby.Naming.TV
|
||||
var numberString = match.Groups["seasonnumber"];
|
||||
if (numberString.Success)
|
||||
{
|
||||
var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
|
||||
return (seasonNumber, true);
|
||||
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
|
||||
{
|
||||
return (seasonNumber, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, false);
|
||||
|
||||
@@ -107,10 +107,20 @@ namespace Emby.Server.Implementations.AppBase
|
||||
|
||||
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
|
||||
{
|
||||
var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName);
|
||||
string? otherMarkers = null;
|
||||
try
|
||||
{
|
||||
otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Error while checking for marker files, assume none exist and keep going
|
||||
// TODO: add some logging
|
||||
}
|
||||
|
||||
if (otherMarkers is not null)
|
||||
{
|
||||
throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}.");
|
||||
throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}.");
|
||||
}
|
||||
|
||||
var markerPath = Path.Combine(path, markerName);
|
||||
|
||||
@@ -1051,16 +1051,16 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
|
||||
.Where(e => e.Value.Length > 0)
|
||||
.Select(i =>
|
||||
{
|
||||
return new NameGuidPair
|
||||
{
|
||||
Name = i.Key,
|
||||
Id = i.Value.First().Id
|
||||
};
|
||||
}).Where(i => i is not null).ToArray();
|
||||
var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||
|
||||
dto.ArtistItems = hasArtist.Artists
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct()
|
||||
.Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
|
||||
? new NameGuidPair { Name = name, Id = artists[0].Id }
|
||||
: null)
|
||||
.Where(item => item is not null)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||
@@ -1085,31 +1085,16 @@ namespace Emby.Server.Implementations.Dto
|
||||
// })
|
||||
// .ToList();
|
||||
|
||||
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||
|
||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||
// .Except(foundArtists, new DistinctNameComparer())
|
||||
.Select(i =>
|
||||
{
|
||||
// This should not be necessary but we're seeing some cases of it
|
||||
if (string.IsNullOrEmpty(i))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
});
|
||||
if (artist is not null)
|
||||
{
|
||||
return new NameGuidPair
|
||||
{
|
||||
Name = artist.Name,
|
||||
Id = artist.Id
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}).Where(i => i is not null).ToArray();
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct()
|
||||
.Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
|
||||
? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
|
||||
: null)
|
||||
.Where(item => item is not null)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// Add video info
|
||||
|
||||
@@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO
|
||||
return;
|
||||
}
|
||||
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||
foreach (var i in _tempIgnoredPaths.Keys)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Security;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -260,7 +261,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true);
|
||||
var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
|
||||
if (targetFileInfo is not null)
|
||||
{
|
||||
result.Exists = targetFileInfo.Exists;
|
||||
@@ -496,8 +497,17 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public virtual bool AreEqual(string path1, string path2)
|
||||
{
|
||||
return Path.TrimEndingDirectorySeparator(path1).Equals(
|
||||
Path.TrimEndingDirectorySeparator(path2),
|
||||
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
|
||||
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
|
||||
|
||||
return string.Equals(
|
||||
normalized1,
|
||||
normalized2,
|
||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
||||
}
|
||||
|
||||
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||
{
|
||||
var age = DateTime.UtcNow - image.DateModified;
|
||||
return age.TotalDays > 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
@@ -11,28 +13,24 @@ namespace Emby.Server.Implementations.Library;
|
||||
/// </summary>
|
||||
public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
private static readonly bool IsWindows = OperatingSystem.IsWindows();
|
||||
|
||||
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
|
||||
{
|
||||
var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore"));
|
||||
if (ignoreFile.Exists)
|
||||
for (var current = directory; current is not null; current = current.Parent)
|
||||
{
|
||||
return ignoreFile;
|
||||
var ignorePath = Path.Join(current.FullName, ".ignore");
|
||||
if (File.Exists(ignorePath))
|
||||
{
|
||||
return new FileInfo(ignorePath);
|
||||
}
|
||||
}
|
||||
|
||||
var parentDir = directory.Parent;
|
||||
if (parentDir is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindIgnoreFile(parentDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
{
|
||||
return IsIgnored(fileInfo, parent);
|
||||
}
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not the file is ignored.
|
||||
@@ -42,65 +40,101 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
/// <returns>True if the file should be ignored.</returns>
|
||||
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
{
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
|
||||
if (dirIgnoreFile is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var searchDirectory = fileInfo.IsDirectory
|
||||
? new DirectoryInfo(fileInfo.FullName)
|
||||
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
|
||||
|
||||
// Fast path in case the ignore files isn't a symlink and is empty
|
||||
if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// ignore the directory only if the .ignore file is empty
|
||||
// evaluate individual files otherwise
|
||||
return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
|
||||
}
|
||||
|
||||
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
|
||||
if (string.IsNullOrEmpty(parentDirPath))
|
||||
if (string.IsNullOrEmpty(searchDirectory.FullName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var folder = new DirectoryInfo(parentDirPath);
|
||||
var ignoreFile = FindIgnoreFile(folder);
|
||||
var ignoreFile = FindIgnoreFile(searchDirectory);
|
||||
if (ignoreFile is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string ignoreFileString = GetFileContent(ignoreFile);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ignoreFileString))
|
||||
// Fast path in case the ignore files isn't a symlink and is empty
|
||||
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
|
||||
{
|
||||
// Ignore directory if we just have the file
|
||||
return true;
|
||||
}
|
||||
|
||||
// If file has content, base ignoring off the content .gitignore-style rules
|
||||
var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var ignore = new Ignore.Ignore();
|
||||
ignore.Add(ignoreRules);
|
||||
|
||||
return ignore.IsIgnored(fileInfo.FullName);
|
||||
var content = GetFileContent(ignoreFile);
|
||||
return string.IsNullOrWhiteSpace(content)
|
||||
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
|
||||
}
|
||||
|
||||
private static string GetFileContent(FileInfo dirIgnoreFile)
|
||||
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
|
||||
{
|
||||
dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
|
||||
if (!dirIgnoreFile.Exists)
|
||||
// If file has content, base ignoring off the content .gitignore-style rules
|
||||
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return CheckIgnoreRules(path, rules, isDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <param name="rules">The array of ignore rules.</param>
|
||||
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||
/// <returns>True if the path should be ignored.</returns>
|
||||
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
|
||||
=> CheckIgnoreRules(path, rules, isDirectory, IsWindows);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <param name="rules">The array of ignore rules.</param>
|
||||
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||
/// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
|
||||
/// <returns>True if the path should be ignored.</returns>
|
||||
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
|
||||
{
|
||||
var ignore = new Ignore.Ignore();
|
||||
|
||||
// Add each rule individually to catch and skip invalid patterns
|
||||
var validRulesAdded = 0;
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
return string.Empty;
|
||||
try
|
||||
{
|
||||
ignore.Add(rule);
|
||||
validRulesAdded++;
|
||||
}
|
||||
catch (RegexParseException)
|
||||
{
|
||||
// Ignore invalid patterns
|
||||
}
|
||||
}
|
||||
|
||||
using (var reader = dirIgnoreFile.OpenText())
|
||||
// If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
|
||||
if (validRulesAdded == 0)
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
|
||||
|
||||
// Add trailing slash for directories to match "folder/"
|
||||
if (isDirectory)
|
||||
{
|
||||
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
|
||||
}
|
||||
|
||||
return ignore.IsIgnored(pathToCheck);
|
||||
}
|
||||
|
||||
private static string GetFileContent(FileInfo ignoreFile)
|
||||
{
|
||||
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
|
||||
return ignoreFile.Exists
|
||||
? File.ReadAllText(ignoreFile.FullName)
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
// Unix hidden files
|
||||
"**/.*",
|
||||
"**/.*/**",
|
||||
|
||||
// Mac - if you ever remove the above.
|
||||
// "**/._*",
|
||||
|
||||
@@ -457,6 +457,12 @@ namespace Emby.Server.Implementations.Library
|
||||
_cache.TryRemove(child.Id, out _);
|
||||
}
|
||||
|
||||
if (parent is Folder folder)
|
||||
{
|
||||
folder.Children = null;
|
||||
folder.UserData = null;
|
||||
}
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
|
||||
@@ -1052,6 +1058,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||
Name = name,
|
||||
UseRawName = true,
|
||||
DtoOptions = options
|
||||
}).Cast<MusicArtist>()
|
||||
.OrderBy(i => i.IsAccessedByName ? 1 : 0)
|
||||
@@ -1993,6 +2000,12 @@ namespace Emby.Server.Implementations.Library
|
||||
RegisterItem(item);
|
||||
}
|
||||
|
||||
if (parent is Folder folder)
|
||||
{
|
||||
folder.Children = null;
|
||||
folder.UserData = null;
|
||||
}
|
||||
|
||||
if (ItemAdded is not null)
|
||||
{
|
||||
foreach (var item in items)
|
||||
@@ -2150,6 +2163,12 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
if (parent is Folder folder)
|
||||
{
|
||||
folder.Children = null;
|
||||
folder.UserData = null;
|
||||
}
|
||||
|
||||
if (ItemUpdated is not null)
|
||||
{
|
||||
foreach (var item in items)
|
||||
@@ -2183,6 +2202,12 @@ namespace Emby.Server.Implementations.Library
|
||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
@@ -3176,19 +3201,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
|
||||
while (File.Exists(lnk))
|
||||
{
|
||||
shortcutFilename += "1";
|
||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
}
|
||||
|
||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||
|
||||
RemoveContentTypeOverrides(path);
|
||||
CreateShortcut(virtualFolderPath, pathInfo);
|
||||
|
||||
if (saveLibraryOptions)
|
||||
{
|
||||
@@ -3353,5 +3366,24 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
||||
}
|
||||
|
||||
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
|
||||
{
|
||||
var path = pathInfo.Path;
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
|
||||
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
|
||||
while (File.Exists(lnk))
|
||||
{
|
||||
shortcutFilename += "1";
|
||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
}
|
||||
|
||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||
RemoveContentTypeOverrides(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <inheritdoc />>
|
||||
public MediaProtocol GetPathProtocol(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return MediaProtocol.File;
|
||||
}
|
||||
|
||||
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MediaProtocol.Rtsp;
|
||||
|
||||
@@ -369,13 +369,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
// We need to only look at the name of this actual item (not parents)
|
||||
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
|
||||
|
||||
if (!justName.IsEmpty)
|
||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||
|
||||
// If not in a mixed folder and ID not found in folder path, check filename
|
||||
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
|
||||
{
|
||||
// Check for TMDb id
|
||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
|
||||
tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
|
||||
}
|
||||
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
|
||||
|
||||
if (!string.IsNullOrEmpty(item.Path))
|
||||
{
|
||||
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo de reproducción: {0}",
|
||||
"Latest": "Recientes",
|
||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Sammlungen",
|
||||
"DeviceOfflineWithName": "{0} wurde getrennt",
|
||||
"DeviceOnlineWithName": "{0} ist verbunden",
|
||||
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
|
||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
||||
"Favorites": "Favorite",
|
||||
"Folders": "Ordner",
|
||||
"Genres": "Genre",
|
||||
|
||||
@@ -129,12 +129,5 @@
|
||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
|
||||
}
|
||||
|
||||
@@ -136,7 +136,5 @@
|
||||
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
||||
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
|
||||
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
|
||||
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалтууд",
|
||||
"Playlists": "Playlist-ууд",
|
||||
"Movies": "Кинонууд",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрлүүд",
|
||||
@@ -71,7 +71,7 @@
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд ТВ",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
@@ -109,7 +109,7 @@
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Шоу",
|
||||
"Sync": "Синхрончлох",
|
||||
"Sync": "Дахин",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
|
||||
@@ -132,10 +132,5 @@
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
|
||||
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
|
||||
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
|
||||
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
|
||||
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
}
|
||||
|
||||
@@ -134,8 +134,6 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
||||
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
||||
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
||||
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
|
||||
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
|
||||
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
|
||||
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
|
||||
}
|
||||
|
||||
@@ -137,6 +137,5 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||
"CleanupUserDataTask": "用戶資料清理工作",
|
||||
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
|
||||
"CleanupUserDataTask": "用戶資料清理工作"
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Emby.Server.Implementations.Localization
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private readonly ConcurrentDictionary<string, CultureDto?> _cultureCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private List<CultureDto> _cultures = [];
|
||||
|
||||
private FrozenDictionary<string, string> _iso6392BtoT = null!;
|
||||
@@ -161,6 +162,7 @@ namespace Emby.Server.Implementations.Localization
|
||||
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
|
||||
}
|
||||
|
||||
_cultureCache.Clear();
|
||||
_cultures = list;
|
||||
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -169,20 +171,31 @@ namespace Emby.Server.Implementations.Localization
|
||||
/// <inheritdoc />
|
||||
public CultureDto? FindLanguageInfo(string language)
|
||||
{
|
||||
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
|
||||
for (var i = 0; i < _cultures.Count; i++)
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
var culture = _cultures[i];
|
||||
if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return culture;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return default;
|
||||
return _cultureCache.GetOrAdd(
|
||||
language,
|
||||
static (lang, cultures) =>
|
||||
{
|
||||
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
|
||||
for (var i = 0; i < cultures.Count; i++)
|
||||
{
|
||||
var culture = cultures[i];
|
||||
if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||
|| lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase)
|
||||
|| lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return culture;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
_cultures);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -311,15 +324,19 @@ namespace Emby.Server.Implementations.Localization
|
||||
else
|
||||
{
|
||||
// Fall back to server default language for ratings check
|
||||
// If it has no ratings, use the US ratings
|
||||
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
|
||||
var ratingsDictionary = GetParentalRatingsDictionary();
|
||||
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't find anything, check all ratings systems
|
||||
// If we don't find anything, check all ratings systems, starting with US
|
||||
if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue))
|
||||
{
|
||||
return usValue;
|
||||
}
|
||||
|
||||
foreach (var dictionary in _allParentalRatings.Values)
|
||||
{
|
||||
if (dictionary.TryGetValue(rating, out var value))
|
||||
|
||||
@@ -347,8 +347,8 @@ pli||pi|Pali|pali
|
||||
pol||pl|Polish|polonais
|
||||
pon|||Pohnpeian|pohnpei
|
||||
por||pt|Portuguese|portugais
|
||||
por||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
|
||||
por||pt-br|Portuguese (Brazil)|portugais (pt-br)
|
||||
pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
|
||||
pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
|
||||
pra|||Prakrit languages|prâkrit, langues
|
||||
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
|
||||
pus||ps|Pushto; Pashto|pachto
|
||||
|
||||
@@ -244,6 +244,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
|
||||
// Update the playlist in the repository
|
||||
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
|
||||
playlist.DateLastMediaAdded = DateTime.UtcNow;
|
||||
|
||||
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates
|
||||
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
_logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||
|
||||
@@ -1625,8 +1625,11 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
|
||||
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
|
||||
|
||||
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
|
||||
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
|
||||
if (state.VideoStream is not null && state.IsOutputVideo)
|
||||
{
|
||||
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
|
||||
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
|
||||
}
|
||||
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
@@ -1836,8 +1839,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
{
|
||||
if (isActualOutputVideoCodecHevc)
|
||||
{
|
||||
// Prefer dvh1 to dvhe
|
||||
args += " -tag:v:0 dvh1 -strict -2";
|
||||
// Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari.
|
||||
var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1";
|
||||
args += $" -tag:v:0 {codecTag} -strict -2";
|
||||
}
|
||||
else if (isActualOutputVideoCodecAv1)
|
||||
{
|
||||
|
||||
@@ -418,7 +418,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Activity;
|
||||
@@ -700,7 +701,18 @@ public class LibraryController : BaseJellyfinApiController
|
||||
// Quotes are valid in linux. They'll possibly cause issues here.
|
||||
var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
|
||||
|
||||
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
|
||||
var filePath = item.Path;
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
// PhysicalFile does not work well with symlinks at the moment.
|
||||
var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true);
|
||||
if (resolved is not null && resolved.Exists)
|
||||
{
|
||||
filePath = resolved.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), filename, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
LibraryOptions options = item.GetLibraryOptions();
|
||||
foreach (var mediaPath in request.LibraryOptions!.PathInfos)
|
||||
{
|
||||
if (options.PathInfos.Any(i => i.Path == mediaPath.Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_libraryManager.CreateShortcut(item.Path, mediaPath);
|
||||
}
|
||||
|
||||
item.UpdateLibraryOptions(request.LibraryOptions);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController
|
||||
[FromRoute, Required] int index,
|
||||
[FromQuery] Guid? mediaSourceId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
|
||||
var item = _libraryManager.GetItemById<BaseItem>(mediaSourceId ?? itemId, User.GetUserId());
|
||||
if (item is null)
|
||||
{
|
||||
return NotFound();
|
||||
|
||||
@@ -154,7 +154,7 @@ public class DynamicHlsHelper
|
||||
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
|
||||
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
|
||||
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
|
||||
newQuery["AudioCodec"] = state.OutputAudioCodec;
|
||||
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
|
||||
}
|
||||
@@ -173,10 +173,21 @@ public class DynamicHlsHelper
|
||||
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
||||
}
|
||||
|
||||
// Main stream
|
||||
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
||||
// Video rotation metadata is only supported in fMP4 remuxing
|
||||
if (state.VideoStream is not null
|
||||
&& state.VideoRequest is not null
|
||||
&& (state.VideoStream?.Rotation ?? 0) != 0
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
||||
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
queryString += "&AllowVideoStreamCopy=false";
|
||||
}
|
||||
|
||||
playlistUrl += queryString;
|
||||
// Main stream
|
||||
var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
||||
var playlistUrl = baseUrl + queryString;
|
||||
var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
|
||||
|
||||
var subtitleStreams = state.MediaSource
|
||||
.MediaStreams
|
||||
@@ -198,37 +209,36 @@ public class DynamicHlsHelper
|
||||
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
||||
}
|
||||
|
||||
// Video rotation metadata is only supported in fMP4 remuxing
|
||||
if (state.VideoStream is not null
|
||||
&& state.VideoRequest is not null
|
||||
&& (state.VideoStream?.Rotation ?? 0) != 0
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
||||
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
playlistUrl += "&AllowVideoStreamCopy=false";
|
||||
}
|
||||
|
||||
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
if (encodingOptions.AllowHevcEncoding
|
||||
&& !encodingOptions.AllowAv1Encoding
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.VideoRange == VideoRange.HDR
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
// Provide AV1 and HEVC SDR entrances for backward compatibility.
|
||||
foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
|
||||
{
|
||||
var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
|
||||
if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
|
||||
var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
|
||||
&& string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
|
||||
var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
|
||||
&& string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
|
||||
var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
|
||||
|
||||
if (isEncodingAllowed
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.VideoRange == VideoRange.HDR)
|
||||
{
|
||||
// Force HEVC Main Profile and disable video stream copy.
|
||||
state.OutputVideoCodec = "hevc";
|
||||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||
// Force AV1 and HEVC Main Profile and disable video stream copy.
|
||||
state.OutputVideoCodec = sdrVideoCodec;
|
||||
|
||||
var sdrPlaylistQuery = playlistQuery;
|
||||
sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
|
||||
sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
|
||||
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
|
||||
|
||||
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
|
||||
|
||||
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
||||
@@ -238,12 +248,30 @@ public class DynamicHlsHelper
|
||||
}
|
||||
}
|
||||
|
||||
// Provide H.264 SDR entrance for backward compatibility.
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.VideoRange == VideoRange.HDR)
|
||||
{
|
||||
// Force H.264 and disable video stream copy.
|
||||
state.OutputVideoCodec = "h264";
|
||||
|
||||
var sdrPlaylistQuery = playlistQuery;
|
||||
sdrPlaylistQuery["VideoCodec"] = "h264";
|
||||
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
|
||||
|
||||
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
|
||||
|
||||
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
}
|
||||
|
||||
// Provide Level 5.0 entrance for backward compatibility.
|
||||
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
||||
// but in fact it is capable of playing videos up to Level 6.1.
|
||||
if (encodingOptions.AllowHevcEncoding
|
||||
&& !encodingOptions.AllowAv1Encoding
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.Level.HasValue
|
||||
&& state.VideoStream.Level > 150
|
||||
&& state.VideoStream.VideoRange == VideoRange.SDR
|
||||
@@ -273,12 +301,15 @@ public class DynamicHlsHelper
|
||||
var variation = GetBitrateVariation(totalBitrate);
|
||||
|
||||
var newBitrate = totalBitrate - variation;
|
||||
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||
var variantQuery = playlistQuery;
|
||||
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
|
||||
var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
|
||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||
|
||||
variation *= 2;
|
||||
newBitrate = totalBitrate - variation;
|
||||
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
|
||||
variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
|
||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||
}
|
||||
|
||||
@@ -863,23 +894,6 @@ public class DynamicHlsHelper
|
||||
return variation;
|
||||
}
|
||||
|
||||
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
|
||||
{
|
||||
return url.Replace(
|
||||
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
|
||||
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
|
||||
{
|
||||
string profileStr = codec + "-profile=";
|
||||
return url.Replace(
|
||||
profileStr + oldValue,
|
||||
profileStr + newValue,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
|
||||
{
|
||||
var oldPlaylist = playlist.ToString();
|
||||
|
||||
@@ -252,6 +252,15 @@ public class MediaInfoHelper
|
||||
options.EnableDirectStream = false;
|
||||
}
|
||||
|
||||
// Force transcoding for remote sources if user has the permission
|
||||
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
|
||||
{
|
||||
options.EnableDirectPlay = false;
|
||||
options.EnableDirectStream = false;
|
||||
options.AllowAudioStreamCopy = false;
|
||||
options.AllowVideoStreamCopy = false;
|
||||
}
|
||||
|
||||
// Beginning of Playback Determination
|
||||
var streamInfo = item.MediaType == MediaType.Audio
|
||||
? streamBuilder.GetOptimalAudioStream(options)
|
||||
|
||||
@@ -159,6 +159,13 @@ public static class StreamingHelpers
|
||||
|
||||
string? containerInternal = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(containerInternal)
|
||||
&& (!string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)
|
||||
|| (mediaSource != null && mediaSource.IsInfiniteStream)))
|
||||
{
|
||||
containerInternal = ".ts";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(streamingRequest.Container))
|
||||
{
|
||||
containerInternal = streamingRequest.Container;
|
||||
|
||||
@@ -75,18 +75,22 @@ public class ExceptionMiddleware
|
||||
if (ignoreStackTrace)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
|
||||
"Error processing request: {ExceptionMessage}. URL {Method} {Url}. Client IP: {RemoteIp}, User-Agent: {UserAgent}",
|
||||
ex.Message.TrimEnd('.'),
|
||||
context.Request.Method,
|
||||
context.Request.Path);
|
||||
context.Request.Path,
|
||||
context.Connection.RemoteIpAddress,
|
||||
context.Request.Headers.UserAgent.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error processing request. URL {Method} {Url}.",
|
||||
"Error processing request. URL {Method} {Url}. Client IP: {RemoteIp}, User-Agent: {UserAgent}",
|
||||
context.Request.Method,
|
||||
context.Request.Path);
|
||||
context.Request.Path,
|
||||
context.Connection.RemoteIpAddress,
|
||||
context.Request.Headers.UserAgent.ToString());
|
||||
}
|
||||
|
||||
context.Response.StatusCode = GetStatusCode(ex);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -275,6 +275,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.StartIndex = filter.StartIndex ?? 0;
|
||||
@@ -294,6 +295,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
@@ -337,6 +339,8 @@ public sealed class BaseItemRepository
|
||||
mainquery = ApplyGroupingFilter(context, mainquery, filter);
|
||||
mainquery = ApplyQueryPaging(mainquery, filter);
|
||||
|
||||
mainquery = ApplyNavigations(mainquery, filter);
|
||||
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
@@ -399,19 +403,32 @@ public sealed class BaseItemRepository
|
||||
dbQuery = dbQuery.Distinct();
|
||||
}
|
||||
|
||||
dbQuery = ApplyOrder(dbQuery, filter);
|
||||
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
dbQuery = ApplyOrder(dbQuery, filter, context);
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.UserData);
|
||||
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.TrailerTypes);
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.Provider);
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.LockedFields);
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.EnableUserData)
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.UserData);
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.EnableImages)
|
||||
{
|
||||
@@ -446,6 +463,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
@@ -599,7 +617,6 @@ public sealed class BaseItemRepository
|
||||
|
||||
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
||||
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
||||
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
|
||||
|
||||
foreach (var item in tuples)
|
||||
{
|
||||
@@ -615,31 +632,24 @@ public sealed class BaseItemRepository
|
||||
{
|
||||
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
|
||||
if (entity.Images is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemImageInfos.AddRange(entity.Images);
|
||||
}
|
||||
|
||||
if (entity.LockedFields is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemMetadataFields.AddRange(entity.LockedFields);
|
||||
}
|
||||
|
||||
context.BaseItems.Attach(entity).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
// reattach old userData entries
|
||||
var userKeys = item.UserDataKey.ToArray();
|
||||
var retentionDate = (DateTime?)null;
|
||||
context.UserData
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||
.ExecuteUpdate(e => e
|
||||
.SetProperty(f => f.ItemId, item.Item.Id)
|
||||
.SetProperty(f => f.RetentionDate, retentionDate));
|
||||
}
|
||||
|
||||
var itemValueMaps = tuples
|
||||
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||
.ToArray();
|
||||
@@ -735,6 +745,29 @@ public sealed class BaseItemRepository
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var userKeys = item.GetUserDataKeys().ToArray();
|
||||
var retentionDate = (DateTime?)null;
|
||||
await dbContext.UserData
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||
.ExecuteUpdateAsync(
|
||||
e => e
|
||||
.SetProperty(f => f.ItemId, item.Id)
|
||||
.SetProperty(f => f.RetentionDate, retentionDate),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BaseItemDto? RetrieveItem(Guid id)
|
||||
{
|
||||
@@ -842,7 +875,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
||||
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
|
||||
dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
|
||||
dto.Studios = entity.Studios?.Split('|') ?? [];
|
||||
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
||||
|
||||
@@ -1004,7 +1037,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
|
||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
|
||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
|
||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
||||
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
||||
@@ -1252,7 +1285,7 @@ public sealed class BaseItemRepository
|
||||
.AsSingleQuery()
|
||||
.Where(e => masterQuery.Contains(e.Id));
|
||||
|
||||
query = ApplyOrder(query, filter);
|
||||
query = ApplyOrder(query, filter, context);
|
||||
|
||||
var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
|
||||
if (filter.EnableTotalRecordCount)
|
||||
@@ -1518,51 +1551,58 @@ public sealed class BaseItemRepository
|
||||
|| query.IncludeItemTypes.Contains(BaseItemKind.Season);
|
||||
}
|
||||
|
||||
private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
|
||||
private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context)
|
||||
{
|
||||
var orderBy = filter.OrderBy;
|
||||
var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
|
||||
var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
|
||||
|
||||
if (hasSearch)
|
||||
{
|
||||
orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
|
||||
orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
|
||||
}
|
||||
else if (orderBy.Count == 0)
|
||||
else if (orderBy.Length == 0)
|
||||
{
|
||||
return query.OrderBy(e => e.SortName);
|
||||
}
|
||||
|
||||
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
||||
|
||||
// When searching, prioritize by match quality: exact match > prefix match > contains
|
||||
if (hasSearch)
|
||||
{
|
||||
orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
|
||||
}
|
||||
|
||||
var firstOrdering = orderBy.FirstOrDefault();
|
||||
if (firstOrdering != default)
|
||||
{
|
||||
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
|
||||
if (firstOrdering.SortOrder == SortOrder.Ascending)
|
||||
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
|
||||
if (orderedQuery is null)
|
||||
{
|
||||
orderedQuery = query.OrderBy(expression);
|
||||
// No search relevance ordering, start fresh
|
||||
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
|
||||
? query.OrderBy(expression)
|
||||
: query.OrderByDescending(expression);
|
||||
}
|
||||
else
|
||||
{
|
||||
orderedQuery = query.OrderByDescending(expression);
|
||||
// Search relevance ordering already applied, chain with ThenBy
|
||||
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
|
||||
? orderedQuery.ThenBy(expression)
|
||||
: orderedQuery.ThenByDescending(expression);
|
||||
}
|
||||
|
||||
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
|
||||
{
|
||||
if (firstOrdering.SortOrder is SortOrder.Ascending)
|
||||
{
|
||||
orderedQuery = orderedQuery.ThenBy(e => e.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
|
||||
}
|
||||
orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
|
||||
? orderedQuery.ThenBy(e => e.Name)
|
||||
: orderedQuery.ThenByDescending(e => e.Name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in orderBy.Skip(1))
|
||||
{
|
||||
var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
|
||||
var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
|
||||
if (item.SortOrder == SortOrder.Ascending)
|
||||
{
|
||||
orderedQuery = orderedQuery!.ThenBy(expression);
|
||||
@@ -1644,19 +1684,18 @@ public sealed class BaseItemRepository
|
||||
var tags = filter.Tags.ToList();
|
||||
var excludeTags = filter.ExcludeTags.ToList();
|
||||
|
||||
if (filter.IsMovie == true)
|
||||
if (filter.IsMovie.HasValue)
|
||||
{
|
||||
if (filter.IncludeItemTypes.Length == 0
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
||||
var shouldIncludeAllMovieTypes = filter.IsMovie.Value
|
||||
&& (filter.IncludeItemTypes.Length == 0
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
|
||||
|
||||
if (!shouldIncludeAllMovieTypes)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.IsMovie);
|
||||
baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
|
||||
}
|
||||
}
|
||||
else if (filter.IsMovie.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
|
||||
}
|
||||
|
||||
if (filter.IsSeries.HasValue)
|
||||
{
|
||||
@@ -1701,15 +1740,16 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrEmpty(filter.SearchTerm))
|
||||
{
|
||||
var searchTerm = filter.SearchTerm.ToLower();
|
||||
if (SearchWildcardTerms.Any(f => searchTerm.Contains(f)))
|
||||
var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
|
||||
var originalSearchTerm = filter.SearchTerm.ToLower();
|
||||
if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
|
||||
{
|
||||
searchTerm = $"%{searchTerm.Trim('%')}%";
|
||||
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), searchTerm)));
|
||||
cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
|
||||
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm)));
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
|
||||
baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1921,8 +1961,15 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Name))
|
||||
{
|
||||
var cleanName = GetCleanValue(filter.Name);
|
||||
baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
|
||||
if (filter.UseRawName == true)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Name == filter.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cleanName = GetCleanValue(filter.Name);
|
||||
baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
|
||||
}
|
||||
}
|
||||
|
||||
// These are the same, for now
|
||||
@@ -1944,19 +1991,20 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith));
|
||||
var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
|
||||
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
|
||||
{
|
||||
// i hate this
|
||||
baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]);
|
||||
var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
|
||||
baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
|
||||
{
|
||||
// i hate this
|
||||
baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]);
|
||||
var lessThanLower = filter.NameLessThan.ToLowerInvariant();
|
||||
baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
|
||||
}
|
||||
|
||||
if (filter.ImageTypes.Length > 0)
|
||||
@@ -2415,40 +2463,24 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ExcludeInheritedTags.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
var excludedTags = filter.ExcludeInheritedTags;
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
|
||||
&& (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))));
|
||||
}
|
||||
|
||||
if (filter.IncludeInheritedTags.Length > 0)
|
||||
{
|
||||
// Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
|
||||
// In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
|
||||
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
||
|
||||
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
||||
}
|
||||
var includeTags = filter.IncludeInheritedTags;
|
||||
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
|
||||
|
||||
// A playlist should be accessible to its owner regardless of allowed tags.
|
||||
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
|
||||
// d ^^ this is stupid it hate this.
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
}
|
||||
// For seasons and episodes, we also need to check the parent series' tags.
|
||||
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)))
|
||||
|
||||
// A playlist should be accessible to its owner regardless of allowed tags
|
||||
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
|
||||
}
|
||||
|
||||
if (filter.SeriesStatuses.Length > 0)
|
||||
@@ -2602,6 +2634,21 @@ public sealed class BaseItemRepository
|
||||
.Where(e => artistNames.Contains(e.Name))
|
||||
.ToArray();
|
||||
|
||||
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
|
||||
var lookup = artists
|
||||
.GroupBy(e => e.Name!)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
|
||||
|
||||
var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
|
||||
foreach (var name in artistNames)
|
||||
{
|
||||
if (lookup.TryGetValue(name, out var artistArray))
|
||||
{
|
||||
result[name] = artistArray;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -18,40 +22,77 @@ public static class OrderMapper
|
||||
/// </summary>
|
||||
/// <param name="sortBy">Item property to sort by.</param>
|
||||
/// <param name="query">Context Query.</param>
|
||||
/// <param name="jellyfinDbContext">Context.</param>
|
||||
/// <returns>Func to be executed later for sorting query.</returns>
|
||||
public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
|
||||
public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext)
|
||||
{
|
||||
return sortBy switch
|
||||
return (sortBy, query.User) switch
|
||||
{
|
||||
ItemSortBy.AirTime => e => e.SortName, // TODO
|
||||
ItemSortBy.Runtime => e => e.RunTimeTicks,
|
||||
ItemSortBy.Random => e => EF.Functions.Random(),
|
||||
ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate,
|
||||
ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
|
||||
ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
|
||||
ItemSortBy.IsFolder => e => e.IsFolder,
|
||||
ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
|
||||
ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
|
||||
ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
|
||||
ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
|
||||
// ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
|
||||
ItemSortBy.SeriesSortName => e => e.SeriesName,
|
||||
(ItemSortBy.AirTime, _) => e => e.SortName, // TODO
|
||||
(ItemSortBy.Runtime, _) => e => e.RunTimeTicks,
|
||||
(ItemSortBy.Random, _) => e => EF.Functions.Random(),
|
||||
(ItemSortBy.DatePlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate,
|
||||
(ItemSortBy.PlayCount, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
|
||||
(ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
|
||||
(ItemSortBy.IsFolder, _) => e => e.IsFolder,
|
||||
(ItemSortBy.IsPlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
|
||||
(ItemSortBy.IsUnplayed, _) => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
|
||||
(ItemSortBy.DateLastContentAdded, _) => e => e.DateLastMediaAdded,
|
||||
(ItemSortBy.Artist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
(ItemSortBy.AlbumArtist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
(ItemSortBy.Studio, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
(ItemSortBy.OfficialRating, _) => e => e.InheritedParentalRatingValue,
|
||||
(ItemSortBy.SeriesSortName, _) => e => e.SeriesName,
|
||||
(ItemSortBy.Album, _) => e => e.Album,
|
||||
(ItemSortBy.DateCreated, _) => e => e.DateCreated,
|
||||
(ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
|
||||
(ItemSortBy.StartDate, _) => e => e.StartDate,
|
||||
(ItemSortBy.Name, _) => e => e.CleanName,
|
||||
(ItemSortBy.CommunityRating, _) => e => e.CommunityRating,
|
||||
(ItemSortBy.ProductionYear, _) => e => e.ProductionYear,
|
||||
(ItemSortBy.CriticRating, _) => e => e.CriticRating,
|
||||
(ItemSortBy.VideoBitRate, _) => e => e.TotalBitrate,
|
||||
(ItemSortBy.ParentIndexNumber, _) => e => e.ParentIndexNumber,
|
||||
(ItemSortBy.IndexNumber, _) => e => e.IndexNumber,
|
||||
(ItemSortBy.SeriesDatePlayed, not null) => e =>
|
||||
jellyfinDbContext.BaseItems
|
||||
.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
|
||||
.Join(jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id && w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
|
||||
.Max(f => f),
|
||||
(ItemSortBy.SeriesDatePlayed, null) => e => jellyfinDbContext.BaseItems.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
|
||||
.Join(jellyfinDbContext.UserData.Where(w => w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
|
||||
.Max(f => f),
|
||||
// ItemSortBy.SeriesDatePlayed => e => jellyfinDbContext.UserData
|
||||
// .Where(u => u.Item!.SeriesPresentationUniqueKey == e.PresentationUniqueKey && u.Played)
|
||||
// .Max(f => f.LastPlayedDate),
|
||||
// ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
|
||||
ItemSortBy.Album => e => e.Album,
|
||||
ItemSortBy.DateCreated => e => e.DateCreated,
|
||||
ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
|
||||
ItemSortBy.StartDate => e => e.StartDate,
|
||||
ItemSortBy.Name => e => e.CleanName,
|
||||
ItemSortBy.CommunityRating => e => e.CommunityRating,
|
||||
ItemSortBy.ProductionYear => e => e.ProductionYear,
|
||||
ItemSortBy.CriticRating => e => e.CriticRating,
|
||||
ItemSortBy.VideoBitRate => e => e.TotalBitrate,
|
||||
ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
|
||||
ItemSortBy.IndexNumber => e => e.IndexNumber,
|
||||
_ => e => e.SortName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an expression to order search results by match quality.
|
||||
/// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
|
||||
/// </summary>
|
||||
/// <param name="searchTerm">The search term to match against.</param>
|
||||
/// <returns>An expression that returns an integer representing match quality (lower is better).</returns>
|
||||
public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm)
|
||||
{
|
||||
var cleanSearchTerm = GetCleanValue(searchTerm);
|
||||
var searchPrefix = cleanSearchTerm + " ";
|
||||
return e =>
|
||||
e.CleanName == cleanSearchTerm ? 0 :
|
||||
e.CleanName!.StartsWith(searchPrefix) ? 1 :
|
||||
e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
|
||||
}
|
||||
|
||||
private static string GetCleanValue(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.RemoveDiacritics().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace Jellyfin.Server.Implementations.StorageHelpers;
|
||||
public static class StorageHelper
|
||||
{
|
||||
private const long TwoGigabyte = 2_147_483_647L;
|
||||
private const long FiveHundredAndTwelveMegaByte = 536_870_911L;
|
||||
private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
|
||||
/// <summary>
|
||||
@@ -24,10 +23,8 @@ public static class StorageHelper
|
||||
public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger)
|
||||
{
|
||||
TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte);
|
||||
TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager
|
||||
}
|
||||
|
||||
// We support video backdrops, but we should not generate trickplay images for them
|
||||
var parentDirectory = Directory.GetParent(mediaPath);
|
||||
var parentDirectory = Directory.GetParent(video.Path);
|
||||
if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
@@ -92,33 +93,38 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork)
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
string pin = BitConverter.ToString(bytes);
|
||||
|
||||
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
|
||||
string filePath = _passwordResetFileBase + user.Id + ".json";
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = filePath,
|
||||
UserName = user.Username
|
||||
};
|
||||
var usernameHash = enteredUsername.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
var pinFile = _passwordResetFileBase + usernameHash + ".json";
|
||||
|
||||
FileStream fileStream = AsyncFile.Create(filePath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
if (user is not null && isInNetwork)
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
|
||||
byte[] bytes = new byte[4];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
string pin = BitConverter.ToString(bytes);
|
||||
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = pinFile,
|
||||
UserName = user.Username
|
||||
};
|
||||
|
||||
FileStream fileStream = AsyncFile.Create(pinFile);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.PinCode,
|
||||
PinExpirationDate = expireTime,
|
||||
PinFile = filePath
|
||||
PinFile = pinFile
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
|
||||
ThrowIfInvalidUsername(newName);
|
||||
|
||||
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
|
||||
if (user.Username.Equals(newName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("The new and old names must be different.");
|
||||
}
|
||||
@@ -508,23 +508,18 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
|
||||
{
|
||||
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
|
||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||
|
||||
var result = await passwordResetProvider
|
||||
.StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (user is not null && isInNetwork)
|
||||
{
|
||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||
var result = await passwordResetProvider
|
||||
.StartForgotPasswordProcess(user, isInNetwork)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.InNetworkRequired,
|
||||
PinFile = string.Empty
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -760,8 +755,13 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
return GetAuthenticationProviders(user)[0];
|
||||
}
|
||||
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User user)
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User? user)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
return _defaultPasswordResetProvider;
|
||||
}
|
||||
|
||||
return GetPasswordResetProviders(user)[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,11 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Interfaces;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
|
||||
|
||||
@@ -259,7 +261,8 @@ namespace Jellyfin.Server.Extensions
|
||||
c.OperationFilter<FileRequestFilter>();
|
||||
c.OperationFilter<ParameterObsoleteFilter>();
|
||||
c.DocumentFilter<AdditionalModelFilter>();
|
||||
});
|
||||
})
|
||||
.Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
|
||||
}
|
||||
|
||||
private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)
|
||||
|
||||
89
Jellyfin.Server/Filters/CachingOpenApiProvider.cs
Normal file
89
Jellyfin.Server/Filters/CachingOpenApiProvider.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// OpenApi provider with caching.
|
||||
/// </summary>
|
||||
internal sealed class CachingOpenApiProvider : ISwaggerProvider
|
||||
{
|
||||
private const string CacheKey = "openapi.json";
|
||||
|
||||
private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
|
||||
private static readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SwaggerGenerator _swaggerGenerator;
|
||||
private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="optionsAccessor">The options accessor.</param>
|
||||
/// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
|
||||
/// <param name="schemaGenerator">The schema generator.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public CachingOpenApiProvider(
|
||||
IOptions<SwaggerGeneratorOptions> optionsAccessor,
|
||||
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
|
||||
ISchemaGenerator schemaGenerator,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_swaggerGeneratorOptions = optionsAccessor.Value;
|
||||
_swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
|
||||
{
|
||||
return AdjustDocument(openApiDocument, host, basePath);
|
||||
}
|
||||
|
||||
var acquired = _lock.Wait(_lockTimeout);
|
||||
try
|
||||
{
|
||||
if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
|
||||
{
|
||||
return AdjustDocument(openApiDocument, host, basePath);
|
||||
}
|
||||
|
||||
if (!acquired)
|
||||
{
|
||||
throw new InvalidOperationException("OpenApi document is generating");
|
||||
}
|
||||
|
||||
openApiDocument = _swaggerGenerator.GetSwagger(documentName);
|
||||
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
|
||||
return AdjustDocument(openApiDocument, host, basePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (acquired)
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
|
||||
{
|
||||
document.Servers = _swaggerGeneratorOptions.Servers.Count != 0
|
||||
? _swaggerGeneratorOptions.Servers
|
||||
: string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath)
|
||||
? []
|
||||
: [new OpenApiServer { Url = $"{host}{basePath}" }];
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
@@ -78,12 +78,15 @@
|
||||
<None Update="wwwroot\api-docs\swagger\custom.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="wwwroot\api-docs\banner-dark.svg">
|
||||
<None Update="wwwroot\api-docs\jellyfin.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ServerSetupApp/index.mstemplate.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources/Shaders/crt_lottes.cl">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,28 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
public void Perform()
|
||||
{
|
||||
var dataPath = _appPaths.DataPath;
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
var dbFilePath = Path.Combine(dataPath, DbFilename);
|
||||
|
||||
if (!File.Exists(dbFilePath))
|
||||
{
|
||||
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Tokens';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'Tokens' doesn't exist in {Path}, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _dbProvider.CreateDbContext();
|
||||
|
||||
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
|
||||
|
||||
@@ -78,9 +78,27 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
||||
|
||||
if (!File.Exists(dbFilePath))
|
||||
{
|
||||
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='userdisplaypreferences';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'userdisplaypreferences' doesn't exist in {Path}, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _provider.CreateDbContext();
|
||||
|
||||
var results = connection.Query("SELECT * FROM userdisplaypreferences");
|
||||
|
||||
@@ -383,8 +383,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
});
|
||||
}
|
||||
|
||||
baseItemIds.Clear();
|
||||
|
||||
foreach (var item in peopleCache)
|
||||
{
|
||||
operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
|
||||
|
||||
@@ -75,7 +75,7 @@ public class MigrateUserDb : IMigrationRoutine
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
340
Jellyfin.Server/Resources/Shaders/crt_lottes.cl
Normal file
340
Jellyfin.Server/Resources/Shaders/crt_lottes.cl
Normal file
@@ -0,0 +1,340 @@
|
||||
// CRT Lottes shader — OpenCL implementation for FFmpeg program_opencl
|
||||
// Port of Timothy Lottes' CRT shader (public domain).
|
||||
// Adapted from the mpv-retro-shaders GLSL version.
|
||||
//
|
||||
// Copyright (c) 2022, The mpv-retro-shaders Contributors
|
||||
// Copyright (c) 2024, Jellyfin Contributors
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// Input/output: NV12 (Y plane + interleaved CbCr plane).
|
||||
// Kernel signature: (src_y, src_uv, dst_y, dst_uv)
|
||||
// FFmpeg program_opencl passes one image2d_t per plane in plane order.
|
||||
|
||||
// fract() requires 2 arguments in OpenCL 1.x; define a 1-arg wrapper.
|
||||
#define crt_fract(x) ((x) - floor(x))
|
||||
|
||||
// ── Parameters ────────────────────────────────────────────────────────────────
|
||||
#ifndef HARD_SCAN
|
||||
#define HARD_SCAN (-8.0f)
|
||||
#endif
|
||||
#ifndef CURVATURE_X
|
||||
#define CURVATURE_X (0.031f)
|
||||
#endif
|
||||
#ifndef CURVATURE_Y
|
||||
#define CURVATURE_Y (0.041f)
|
||||
#endif
|
||||
#ifndef MASK_DARK
|
||||
#define MASK_DARK (0.5f)
|
||||
#endif
|
||||
#ifndef MASK_LIGHT
|
||||
#define MASK_LIGHT (1.5f)
|
||||
#endif
|
||||
#ifndef SHADOW_MASK
|
||||
#define SHADOW_MASK 2
|
||||
#endif
|
||||
#ifndef BRIGHTNESS_BOOST
|
||||
#define BRIGHTNESS_BOOST (1.0f)
|
||||
#endif
|
||||
#ifndef HARD_BLOOM_SCAN
|
||||
#define HARD_BLOOM_SCAN (-2.0f)
|
||||
#endif
|
||||
#ifndef BLOOM_AMOUNT
|
||||
#define BLOOM_AMOUNT (1.0f / 16.0f)
|
||||
#endif
|
||||
#ifndef SHAPE
|
||||
#define SHAPE (2.0f)
|
||||
#endif
|
||||
|
||||
// ── sRGB ↔ linear ─────────────────────────────────────────────────────────────
|
||||
|
||||
static float3 linearize_rgb(float3 c)
|
||||
{
|
||||
const float k0 = 0.05958483740687370300f;
|
||||
const float k1 = 0.87031054496765136718f;
|
||||
c = max(c, 0.0f);
|
||||
c = k1 * pow(c + (float3)(k0), (float3)(2.4f));
|
||||
return c;
|
||||
}
|
||||
|
||||
static float3 delinearize_rgb(float3 c)
|
||||
{
|
||||
const float k0 = 0.05958483740687370300f;
|
||||
const float k1 = 1.14901518821716308593f;
|
||||
c = max(c, 0.0f);
|
||||
c = pow(k1 * c, (float3)(1.0f / 2.4f)) - (float3)(k0);
|
||||
return c;
|
||||
}
|
||||
|
||||
// ── NV12 fetch helper ─────────────────────────────────────────────────────────
|
||||
// Reads Y + CbCr from NV12 planes and returns linearised RGB.
|
||||
// Both planes use the same normalised coordinates [0,1]; the UV plane is
|
||||
// half-resolution but the sampler maps the same normalised position to the
|
||||
// corresponding chroma sample automatically.
|
||||
|
||||
static float3 fetch_linear_nv12(
|
||||
__read_only image2d_t y_plane,
|
||||
__read_only image2d_t uv_plane,
|
||||
sampler_t smp,
|
||||
float2 pos,
|
||||
float2 off_texels,
|
||||
float2 src_size)
|
||||
{
|
||||
float2 p = pos + off_texels / src_size;
|
||||
float y = BRIGHTNESS_BOOST * read_imagef(y_plane, smp, p).x;
|
||||
float2 cbcr = read_imagef(uv_plane, smp, p).xy - 0.5f; // centre Cb/Cr
|
||||
|
||||
// BT.709 full-range YCbCr → RGB
|
||||
float r = clamp(y + 1.5748f * cbcr.y, 0.0f, 1.0f);
|
||||
float g = clamp(y - 0.1873f * cbcr.x - 0.4681f * cbcr.y, 0.0f, 1.0f);
|
||||
float b = clamp(y + 1.8556f * cbcr.x, 0.0f, 1.0f);
|
||||
return linearize_rgb((float3)(r, g, b));
|
||||
}
|
||||
|
||||
// ── Gaussian kernel ───────────────────────────────────────────────────────────
|
||||
|
||||
static float gauss1d(float pos, float scale)
|
||||
{
|
||||
return exp2(scale * pow(fabs(pos), SHAPE));
|
||||
}
|
||||
|
||||
static float2 distance_to_texel(float2 pos, float2 src_size)
|
||||
{
|
||||
return -1.0f * crt_fract(pos * src_size - 0.5f);
|
||||
}
|
||||
|
||||
// ── Horizontal reconstruction (3 / 5 / 7 tap) ────────────────────────────────
|
||||
|
||||
static float3 horz3(
|
||||
__read_only image2d_t y_plane,
|
||||
__read_only image2d_t uv_plane,
|
||||
sampler_t smp,
|
||||
float2 pos, float off_y, float scale, float2 src_size)
|
||||
{
|
||||
float3 c = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)(-1.0f, off_y), src_size);
|
||||
float3 d = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 0.0f, off_y), src_size);
|
||||
float3 e = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 1.0f, off_y), src_size);
|
||||
float dst = distance_to_texel(pos, src_size).x;
|
||||
float wc = gauss1d(dst - 1.0f, scale);
|
||||
float wd = gauss1d(dst, scale);
|
||||
float we = gauss1d(dst + 1.0f, scale);
|
||||
return (c * wc + d * wd + e * we) / (wc + wd + we);
|
||||
}
|
||||
|
||||
static float3 horz5(
|
||||
__read_only image2d_t y_plane,
|
||||
__read_only image2d_t uv_plane,
|
||||
sampler_t smp,
|
||||
float2 pos, float off_y, float scale, float2 src_size)
|
||||
{
|
||||
float3 b = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)(-2.0f, off_y), src_size);
|
||||
float3 c = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)(-1.0f, off_y), src_size);
|
||||
float3 d = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 0.0f, off_y), src_size);
|
||||
float3 e = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 1.0f, off_y), src_size);
|
||||
float3 f = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 2.0f, off_y), src_size);
|
||||
float dst = distance_to_texel(pos, src_size).x;
|
||||
float wb = gauss1d(dst - 2.0f, scale);
|
||||
float wc = gauss1d(dst - 1.0f, scale);
|
||||
float wd = gauss1d(dst, scale);
|
||||
float we = gauss1d(dst + 1.0f, scale);
|
||||
float wf = gauss1d(dst + 2.0f, scale);
|
||||
return (b * wb + c * wc + d * wd + e * we + f * wf) / (wb + wc + wd + we + wf);
|
||||
}
|
||||
|
||||
static float3 horz7(
|
||||
__read_only image2d_t y_plane,
|
||||
__read_only image2d_t uv_plane,
|
||||
sampler_t smp,
|
||||
float2 pos, float off_y, float scale, float2 src_size)
|
||||
{
|
||||
float3 a = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)(-3.0f, off_y), src_size);
|
||||
float3 b = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)(-2.0f, off_y), src_size);
|
||||
float3 c = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)(-1.0f, off_y), src_size);
|
||||
float3 d = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 0.0f, off_y), src_size);
|
||||
float3 e = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 1.0f, off_y), src_size);
|
||||
float3 f = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 2.0f, off_y), src_size);
|
||||
float3 g = fetch_linear_nv12(y_plane, uv_plane, smp, pos, (float2)( 3.0f, off_y), src_size);
|
||||
float dst = distance_to_texel(pos, src_size).x;
|
||||
float wa = gauss1d(dst - 3.0f, scale);
|
||||
float wb = gauss1d(dst - 2.0f, scale);
|
||||
float wc = gauss1d(dst - 1.0f, scale);
|
||||
float wd = gauss1d(dst, scale);
|
||||
float we = gauss1d(dst + 1.0f, scale);
|
||||
float wf = gauss1d(dst + 2.0f, scale);
|
||||
float wg = gauss1d(dst + 3.0f, scale);
|
||||
return (a * wa + b * wb + c * wc + d * wd + e * we + f * wf + g * wg)
|
||||
/ (wa + wb + wc + wd + we + wf + wg);
|
||||
}
|
||||
|
||||
// ── Screen curvature ──────────────────────────────────────────────────────────
|
||||
|
||||
static float2 bend_screen(float2 pos)
|
||||
{
|
||||
pos = pos * 2.0f - 1.0f;
|
||||
pos *= (float2)(1.0f + (pos.y * pos.y) * CURVATURE_X,
|
||||
1.0f + (pos.x * pos.x) * CURVATURE_Y);
|
||||
return pos * 0.5f + 0.5f;
|
||||
}
|
||||
|
||||
// ── Scanline weights ──────────────────────────────────────────────────────────
|
||||
|
||||
static float scan_weight(float2 pos, float off, float2 src_size)
|
||||
{
|
||||
float dst = distance_to_texel(pos, src_size).y;
|
||||
return gauss1d(dst + off, HARD_SCAN);
|
||||
}
|
||||
|
||||
static float bloom_scan_weight(float2 pos, float off, float2 src_size)
|
||||
{
|
||||
float dst = distance_to_texel(pos, src_size).y;
|
||||
return gauss1d(dst + off, HARD_BLOOM_SCAN);
|
||||
}
|
||||
|
||||
// ── Main CRT reconstruction ───────────────────────────────────────────────────
|
||||
|
||||
static float3 tri(
|
||||
__read_only image2d_t y_plane,
|
||||
__read_only image2d_t uv_plane,
|
||||
sampler_t smp,
|
||||
float2 pos, float2 src_size)
|
||||
{
|
||||
float3 a = horz3(y_plane, uv_plane, smp, pos, -1.0f, -10.0f, src_size);
|
||||
float3 b = horz5(y_plane, uv_plane, smp, pos, 0.0f, -10.0f, src_size);
|
||||
float3 c = horz3(y_plane, uv_plane, smp, pos, 1.0f, -10.0f, src_size);
|
||||
float wa = scan_weight(pos, -1.0f, src_size);
|
||||
float wb = scan_weight(pos, 0.0f, src_size);
|
||||
float wc = scan_weight(pos, 1.0f, src_size);
|
||||
return a * wa + b * wb + c * wc;
|
||||
}
|
||||
|
||||
static float3 bloom(
|
||||
__read_only image2d_t y_plane,
|
||||
__read_only image2d_t uv_plane,
|
||||
sampler_t smp,
|
||||
float2 pos, float2 src_size)
|
||||
{
|
||||
float3 a = horz5(y_plane, uv_plane, smp, pos, -2.0f, -3.0f, src_size);
|
||||
float3 b = horz7(y_plane, uv_plane, smp, pos, -1.0f, -1.5f, src_size);
|
||||
float3 c = horz7(y_plane, uv_plane, smp, pos, 0.0f, -1.5f, src_size);
|
||||
float3 d = horz7(y_plane, uv_plane, smp, pos, 1.0f, -1.5f, src_size);
|
||||
float3 e = horz5(y_plane, uv_plane, smp, pos, 2.0f, -3.0f, src_size);
|
||||
float wa = bloom_scan_weight(pos, -2.0f, src_size);
|
||||
float wb = bloom_scan_weight(pos, -1.0f, src_size);
|
||||
float wc = bloom_scan_weight(pos, 0.0f, src_size);
|
||||
float wd = bloom_scan_weight(pos, 1.0f, src_size);
|
||||
float we = bloom_scan_weight(pos, 2.0f, src_size);
|
||||
return a * wa + b * wb + c * wc + d * wd + e * we;
|
||||
}
|
||||
|
||||
// ── Shadow mask ───────────────────────────────────────────────────────────────
|
||||
|
||||
static float3 apply_mask(float2 px)
|
||||
{
|
||||
float3 m = (float3)(MASK_DARK);
|
||||
|
||||
#if SHADOW_MASK == 1
|
||||
float line = MASK_LIGHT;
|
||||
float odd = (crt_fract(px.x / 6.0f) < 0.5f) ? 1.0f : 0.0f;
|
||||
if (crt_fract((px.y + odd) / 2.0f) < 0.5f) line = MASK_DARK;
|
||||
float mx = crt_fract(px.x / 3.0f);
|
||||
if (mx < 1.0f / 3.0f) m.x = MASK_LIGHT;
|
||||
else if (mx < 2.0f / 3.0f) m.y = MASK_LIGHT;
|
||||
else m.z = MASK_LIGHT;
|
||||
m *= line;
|
||||
|
||||
#elif SHADOW_MASK == 2
|
||||
float mx2 = crt_fract(px.x / 3.0f);
|
||||
if (mx2 < 1.0f / 3.0f) m.x = MASK_LIGHT;
|
||||
else if (mx2 < 2.0f / 3.0f) m.y = MASK_LIGHT;
|
||||
else m.z = MASK_LIGHT;
|
||||
|
||||
#elif SHADOW_MASK == 3
|
||||
px.x += px.y * 3.0f;
|
||||
float mx3 = crt_fract(px.x / 6.0f);
|
||||
if (mx3 < 1.0f / 3.0f) m.x = MASK_LIGHT;
|
||||
else if (mx3 < 2.0f / 3.0f) m.y = MASK_LIGHT;
|
||||
else m.z = MASK_LIGHT;
|
||||
|
||||
#elif SHADOW_MASK == 4
|
||||
px = floor(px * (float2)(1.0f, 0.5f));
|
||||
px.x += px.y * 3.0f;
|
||||
float mx4 = crt_fract(px.x / 6.0f);
|
||||
if (mx4 < 1.0f / 3.0f) m.x = MASK_LIGHT;
|
||||
else if (mx4 < 2.0f / 3.0f) m.y = MASK_LIGHT;
|
||||
else m.z = MASK_LIGHT;
|
||||
#endif
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
// NV12: FFmpeg program_opencl passes planes in order, so for 2-plane NV12:
|
||||
// arg 0 = src_y (input Y, R channel, full resolution)
|
||||
// arg 1 = src_uv (input UV, RG channels, half resolution)
|
||||
// arg 2 = dst_y (output Y, full resolution)
|
||||
// arg 3 = dst_uv (output UV, half resolution)
|
||||
// Global work size is set to dst_y dimensions (full resolution).
|
||||
|
||||
__kernel void crt_lottes(
|
||||
__read_only image2d_t src_y,
|
||||
__read_only image2d_t src_uv,
|
||||
__write_only image2d_t dst_y,
|
||||
__write_only image2d_t dst_uv)
|
||||
{
|
||||
int2 coord = (int2)(get_global_id(0), get_global_id(1));
|
||||
|
||||
const int dst_w = get_image_width(dst_y);
|
||||
const int dst_h = get_image_height(dst_y);
|
||||
|
||||
if (coord.x >= dst_w || coord.y >= dst_h)
|
||||
return;
|
||||
|
||||
const int src_w = get_image_width(src_y);
|
||||
const int src_h = get_image_height(src_y);
|
||||
|
||||
const float2 dst_size = (float2)(dst_w, dst_h);
|
||||
const float2 src_size = (float2)(src_w, src_h);
|
||||
|
||||
// Normalised position in output space
|
||||
const float2 out_pos = ((float2)(coord.x, coord.y) + 0.5f) / dst_size;
|
||||
|
||||
// Sampler: normalised coords, linear filter, clamp-to-edge
|
||||
const sampler_t smp =
|
||||
CLK_NORMALIZED_COORDS_TRUE |
|
||||
CLK_ADDRESS_CLAMP_TO_EDGE |
|
||||
CLK_FILTER_LINEAR;
|
||||
|
||||
// Apply CRT barrel-curvature
|
||||
float2 bent = bend_screen(out_pos);
|
||||
|
||||
// Main scanline reconstruction + bloom
|
||||
float3 color = tri(src_y, src_uv, smp, bent, src_size);
|
||||
color += bloom(src_y, src_uv, smp, bent, src_size) * BLOOM_AMOUNT;
|
||||
|
||||
// Shadow mask
|
||||
#if SHADOW_MASK != 0
|
||||
float2 px = floor(out_pos * dst_size) + 0.5f;
|
||||
color *= apply_mask(px);
|
||||
#endif
|
||||
|
||||
// Black outside the curved screen border
|
||||
int in_bounds = (bent.x >= 0.0f && bent.x <= 1.0f &&
|
||||
bent.y >= 0.0f && bent.y <= 1.0f) ? 1 : 0;
|
||||
|
||||
float3 rgb = in_bounds ? delinearize_rgb(color) : (float3)(0.0f);
|
||||
|
||||
// BT.709 full-range RGB → YCbCr
|
||||
float y_out = 0.2126f * rgb.x + 0.7152f * rgb.y + 0.0722f * rgb.z;
|
||||
float cb_out = -0.1146f * rgb.x - 0.3854f * rgb.y + 0.5000f * rgb.z + 0.5f;
|
||||
float cr_out = 0.5000f * rgb.x - 0.4542f * rgb.y - 0.0458f * rgb.z + 0.5f;
|
||||
|
||||
// Write Y at full resolution
|
||||
write_imagef(dst_y, coord, (float4)(y_out, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
// Write UV at half resolution (one thread per 2x2 Y block, no races)
|
||||
if ((coord.x & 1) == 0 && (coord.y & 1) == 0) {
|
||||
write_imagef(dst_uv, coord >> 1, (float4)(cb_out, cr_out, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- ***** BEGIN LICENSE BLOCK *****
|
||||
- Part of the Jellyfin project (https://jellyfin.media)
|
||||
-
|
||||
- All copyright belongs to the Jellyfin contributors; a full list can
|
||||
- be found in the file CONTRIBUTORS.md
|
||||
-
|
||||
- This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
- To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/.
|
||||
- ***** END LICENSE BLOCK ***** -->
|
||||
<svg id="banner-dark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1536 512">
|
||||
<defs>
|
||||
<linearGradient id="linear-gradient" x1="110.25" y1="213.3" x2="496.14" y2="436.09" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#aa5cc3"/>
|
||||
<stop offset="1" stop-color="#00a4dc"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title>banner-dark</title>
|
||||
<g id="banner-dark">
|
||||
<g id="banner-dark-icon">
|
||||
<path id="inner-shape" d="M261.42,201.62c-20.44,0-86.24,119.29-76.2,139.43s142.48,19.92,152.4,0S281.86,201.63,261.42,201.62Z" fill="url(#linear-gradient)"/>
|
||||
<path id="outer-shape" d="M261.42,23.3C199.83,23.3,1.57,382.73,31.8,443.43s429.34,60,459.24,0S323,23.3,261.42,23.3ZM411.9,390.76c-19.59,39.33-281.08,39.77-300.9,0S221.1,115.48,261.45,115.48,431.49,351.42,411.9,390.76Z" fill="url(#linear-gradient)"/>
|
||||
</g>
|
||||
<g id="jellyfin-light-outlines" style="isolation:isolate" transform="translate(43.8)">
|
||||
<path d="M556.64,350.75a67,67,0,0,1-22.87-27.47,8.91,8.91,0,0,1-1.49-4.75,7.42,7.42,0,0,1,2.83-5.94,9.25,9.25,0,0,1,6.09-2.38c3.16,0,5.94,1.69,8.31,5.05a48.09,48.09,0,0,0,16.34,20.34,40.59,40.59,0,0,0,24,7.58q20.51,0,33.27-12.62t12.77-33.12V159a8.44,8.44,0,0,1,2.67-6.39,9.56,9.56,0,0,1,6.83-2.52,9,9,0,0,1,6.68,2.52,8.7,8.7,0,0,1,2.53,6.39v138.4a64.7,64.7,0,0,1-8.32,32.67,59,59,0,0,1-23,22.72Q608.62,361,589.9,361A57.21,57.21,0,0,1,556.64,350.75Z" fill="#fff"/>
|
||||
<path d="M831.66,279.47a8.77,8.77,0,0,1-6.24,2.53H713.16q0,17.82,7.27,31.92a54.91,54.91,0,0,0,20.79,22.28q13.51,8.18,31.93,8.17a54,54,0,0,0,25.54-5.94,52.7,52.7,0,0,0,18.12-15.15,10,10,0,0,1,6.24-2.67,8.14,8.14,0,0,1,7.72,7.72,8.81,8.81,0,0,1-3,6.24,74.7,74.7,0,0,1-23.91,19A65.56,65.56,0,0,1,773.45,361q-22.87,0-40.4-9.8a69.51,69.51,0,0,1-27.32-27.48q-9.79-17.66-9.8-40.83,0-24.36,9.65-42.62t25.69-27.92a65.2,65.2,0,0,1,34.16-9.65A70,70,0,0,1,798.84,211a65.78,65.78,0,0,1,25.39,24.36q9.81,16,10.1,38A8.07,8.07,0,0,1,831.66,279.47ZM733.5,231.8Q718.8,243.68,714.64,266H815.92v-2.38A46.91,46.91,0,0,0,807,240.27a48.47,48.47,0,0,0-18.56-15.15,54,54,0,0,0-23-5.2Q748.2,219.92,733.5,231.8Z" fill="#fff"/>
|
||||
<path d="M888.24,355.5a8.92,8.92,0,0,1-15.3-6.38v-202a8.91,8.91,0,1,1,17.82,0v202A8.65,8.65,0,0,1,888.24,355.5Z" fill="#fff"/>
|
||||
<path d="M956.55,355.5a8.92,8.92,0,0,1-15.3-6.38v-202a8.91,8.91,0,1,1,17.82,0v202A8.65,8.65,0,0,1,956.55,355.5Z" fill="#fff"/>
|
||||
<path d="M1122.86,206.11a8.7,8.7,0,0,1,2.53,6.39v131q0,23.44-9.21,40.09a61.58,61.58,0,0,1-25.54,25.25q-16.34,8.61-36.83,8.61a96.73,96.73,0,0,1-23.31-2.68,61.72,61.72,0,0,1-18-7.12q-6.24-3.87-6.24-8.62a17.94,17.94,0,0,1,.6-3,8.06,8.06,0,0,1,3-4.45,7.49,7.49,0,0,1,4.45-1.49,7.91,7.91,0,0,1,3.56.89q19,10.39,36.24,10.4,24.65,0,39.06-15.44t14.4-42.18V333.38a54.37,54.37,0,0,1-21.38,20,62.55,62.55,0,0,1-30.3,7.58q-25.83,0-39.2-15.45t-13.37-41.87V212.5a8.91,8.91,0,1,1,17.82,0V301q0,21.39,9.36,32.38t29.25,11a48,48,0,0,0,23.32-6.09,49.88,49.88,0,0,0,17.82-16,37.44,37.44,0,0,0,6.68-21.24V212.5a9,9,0,0,1,15.29-6.39Z" fill="#fff"/>
|
||||
<path d="M1210.18,161.41q-5.21,6.24-5.2,17.23v30.59h33.27a8.19,8.19,0,0,1,5.79,2.38,8.26,8.26,0,0,1,0,11.88,8.22,8.22,0,0,1-5.79,2.37H1205V349.12a8.91,8.91,0,1,1-17.82,0V225.86h-21.68a7.83,7.83,0,0,1-5.94-2.52,8.21,8.21,0,0,1-2.37-5.79,8,8,0,0,1,2.37-6.09,8.33,8.33,0,0,1,5.94-2.23h21.68V178.64q0-18.7,10.84-29t29-10.24a46.1,46.1,0,0,1,15.45,2.52q7.13,2.53,7.12,8.17a8.07,8.07,0,0,1-2.37,5.94,7.37,7.37,0,0,1-5.35,2.37,18.81,18.81,0,0,1-6.53-1.48,42,42,0,0,0-10.4-1.78Q1215.37,155.18,1210.18,161.41ZM1276,180.87c-2.19-1.88-3.27-4.61-3.27-8.17v-3q0-5.34,3.41-8.17t9.36-2.82q11.88,0,11.88,11v3c0,3.56-1,6.29-3.12,8.17s-5.1,2.82-9.06,2.82S1278.14,182.75,1276,180.87Zm15.59,174.63a8.92,8.92,0,0,1-15.3-6.38V212.5a8.91,8.91,0,1,1,17.82,0V349.12A8.65,8.65,0,0,1,1291.56,355.5Z" fill="#fff"/>
|
||||
<path d="M1452.53,218.88q12.92,16.2,12.92,42.92v87.32a8.4,8.4,0,0,1-2.67,6.38,8.8,8.8,0,0,1-6.24,2.53,8.64,8.64,0,0,1-8.91-8.91V262.69q0-19.31-9.65-31.33t-29.85-12a53.28,53.28,0,0,0-42.77,21.83,36.24,36.24,0,0,0-7.13,21.53v86.43a8.91,8.91,0,1,1-17.82,0V216.06a8.91,8.91,0,1,1,17.82,0V232.4q8-12.77,23-21.24A61.84,61.84,0,0,1,1412,202.7Q1439.61,202.7,1452.53,218.88Z" fill="#fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
26
Jellyfin.Server/wwwroot/api-docs/jellyfin.svg
Normal file
26
Jellyfin.Server/wwwroot/api-docs/jellyfin.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="251" height="72" fill="none" viewBox="0 0 251 72">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="url(#b)"
|
||||
d="M24.212 49.158C22.66 46.042 32.838 27.588 36 27.588c3.167.002 13.323 18.488 11.788 21.57-1.534 3.082-22.025 3.116-23.576 0" />
|
||||
<path fill="url(#c)" fill-rule="evenodd"
|
||||
d="M.482 64.995C-4.195 55.605 26.477 0 36 0c9.533 0 40.153 55.713 35.527 64.995s-66.368 9.39-71.045 0m12.254-8.148c3.064 6.152 43.518 6.084 46.548 0 3.03-6.086-17.032-42.586-23.275-42.586S9.671 50.694 12.736 56.847"
|
||||
clip-rule="evenodd" />
|
||||
<path fill="#fff"
|
||||
d="M225.22 56c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.219-.218c-.054-.107-.054-.247-.054-.527V26.8c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.183c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527v2.895a7.9 7.9 0 0 1 3.419-3.254q2.261-1.103 5.074-1.103 3.308 0 5.845 1.434a10.1 10.1 0 0 1 4.026 4.026q1.434 2.536 1.434 5.9V55.2c0 .28 0 .42-.055.527a.5.5 0 0 1-.218.218c-.107.055-.247.055-.527.055h-5.625c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.218-.218c-.055-.107-.055-.247-.055-.527V38.408q0-2.978-1.709-4.688-1.654-1.764-4.357-1.764-2.702 0-4.412 1.764-1.654 1.766-1.654 4.688V55.2c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.218c-.107.055-.247.055-.527.055zm-11.54-33.363c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.218-.218c-.055-.107-.055-.247-.055-.527v-6.121c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .219.219c.054.107.054.247.054.527v6.12c0 .28 0 .42-.054.528a.5.5 0 0 1-.219.218c-.107.055-.247.055-.527.055zm0 33.363c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.218-.219c-.055-.107-.055-.247-.055-.527V26.8c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.218c.107-.055.247-.055.527-.055h5.624c.28 0 .42 0 .527.055a.5.5 0 0 1 .219.218c.054.107.054.247.054.527v28.4c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.219c-.107.054-.247.054-.527.054zm-16.712-.054c.107.054.247.054.527.054h5.625c.28 0 .42 0 .526-.054a.5.5 0 0 0 .219-.219c.055-.107.055-.247.055-.527V32.452h5.872c.28 0 .42 0 .527-.054a.5.5 0 0 0 .219-.219c.054-.107.054-.247.054-.527V26.8c0-.28 0-.42-.054-.527a.5.5 0 0 0-.219-.218c-.107-.055-.247-.055-.527-.055h-5.872v-.992q0-2.261 1.323-3.31 1.379-1.102 3.75-1.102.454 0 .939.044c.345.031.518.047.634-.004a.48.48 0 0 0 .241-.22c.061-.111.061-.274.061-.6V15.39c0-.304 0-.457-.061-.589a.7.7 0 0 0-.248-.284c-.122-.078-.261-.097-.537-.136a14.5 14.5 0 0 0-1.966-.126q-5.184 0-8.273 2.812t-3.088 7.942V26H186.53c-.3 0-.451 0-.58.05a.75.75 0 0 0-.296.205c-.091.104-.143.244-.248.526l-7.43 19.9-7.483-19.903c-.105-.28-.158-.42-.249-.524a.75.75 0 0 0-.296-.205c-.129-.049-.279-.049-.578-.049h-5.769c-.394 0-.591 0-.717.083a.5.5 0 0 0-.213.314c-.031.147.041.33.186.697L174.281 56l-.661 1.6q-.883 1.874-2.041 3.033-1.103 1.158-3.584 1.158-.883 0-1.875-.166a13 13 0 0 1-.73-.1c-.389-.066-.584-.099-.709-.053a.47.47 0 0 0-.26.22c-.066.116-.066.298-.066.663v4.329c0 .243 0 .365.045.481a.7.7 0 0 0 .189.266c.095.081.194.116.392.185q.684.24 1.47.351 1.158.22 2.371.22 4.246 0 7.059-2.426 2.867-2.37 4.577-6.728l10.517-26.58h5.72V55.2c0 .28 0 .42.055.527a.5.5 0 0 0 .218.219M154.363 56c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.219-.219c-.054-.107-.054-.247-.054-.527V15.054c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527V55.2c0 .28 0 .42-.055.527a.5.5 0 0 1-.218.219c-.107.054-.247.054-.527.054zm-11.621 0c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.219-.219c-.054-.107-.054-.247-.054-.527V15.054c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .219.219c.054.107.054.247.054.527V55.2c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.219c-.107.054-.247.054-.527.054zm-18.132.662q-4.632-.001-8.107-2.096a14.6 14.6 0 0 1-5.404-5.68q-1.93-3.585-1.93-7.942 0-4.522 1.93-7.996 1.985-3.53 5.349-5.57 3.42-2.04 7.61-2.04 4.688 0 7.942 2.04 3.253 1.986 4.963 5.294 1.71 3.309 1.709 7.335 0 .828-.11 1.654-.031.45-.12.841c-.037.165-.055.247-.115.33a.55.55 0 0 1-.208.168c-.095.04-.194.04-.393.04h-21.057q.33 3.309 2.537 5.294 2.205 1.986 5.459 1.985 2.482 0 4.191-1.047a8.2 8.2 0 0 0 2.206-1.986c.241-.316.362-.474.484-.542a.6.6 0 0 1 .352-.083c.139.006.296.083.608.236l4.269 2.094c.239.118.359.176.431.275a.52.52 0 0 1 .098.298c0 .122-.058.231-.172.45q-1.432 2.742-4.526 4.607-3.419 2.04-7.996 2.04m-.552-25.368q-2.702 0-4.687 1.654-1.93 1.6-2.537 4.577h14.118q-.22-2.757-2.151-4.466-1.875-1.765-4.743-1.765M90.801 56c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.218-.218C90 55.62 90 55.48 90 55.2v-5.294c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.218c.107-.055.247-.055.527-.055h1.572q2.646 0 4.19-1.489 1.6-1.545 1.6-4.08V15.715c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.219c.107-.054.247-.054.527-.054h5.956c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527v27.546q0 3.804-1.655 6.672-1.599 2.868-4.632 4.467-2.979 1.6-7.06 1.6z" />
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="12" x2="71.999" y1="30.001" y2="63.002"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#aa5cc3" />
|
||||
<stop offset="1" stop-color="#00a4dc" />
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="12" x2="71.999" y1="29.999" y2="63.001"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#aa5cc3" />
|
||||
<stop offset="1" stop-color="#00a4dc" />
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h251v72H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -4,12 +4,14 @@
|
||||
}
|
||||
|
||||
.topbar-wrapper .link:after {
|
||||
content: url(../banner-dark.svg);
|
||||
content: '';
|
||||
display: block;
|
||||
-moz-box-sizing: border-box;
|
||||
background-image: url(../jellyfin.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: 150px;
|
||||
width: 220px;
|
||||
height: 40px;
|
||||
}
|
||||
/* end logo */
|
||||
|
||||
@@ -103,11 +103,11 @@ namespace MediaBrowser.Common.Configuration
|
||||
void MakeSanityCheckOrThrow();
|
||||
|
||||
/// <summary>
|
||||
/// Checks and creates the given path and adds it with a marker file if non existant.
|
||||
/// Checks and creates the given path and adds it with a marker file if non existent.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <param name="markerName">The common marker file name.</param>
|
||||
/// <param name="recursive">Check for other settings paths recursivly.</param>
|
||||
/// <param name="recursive">Check for other settings paths recursively.</param>
|
||||
void CreateAndCheckMarker(string path, string markerName, bool recursive = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -15,11 +13,12 @@ namespace MediaBrowser.Controller.Authentication
|
||||
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
|
||||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork);
|
||||
|
||||
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
public class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
|
||||
@@ -24,6 +24,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
@@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
var protocol = item.PathProtocol;
|
||||
|
||||
// Resolve the item path so everywhere we use the media source it will always point to
|
||||
// the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
|
||||
// path will return null, so it's safe to check for all paths.
|
||||
var itemPath = item.Path;
|
||||
if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo)
|
||||
{
|
||||
itemPath = linkInfo.FullName;
|
||||
}
|
||||
|
||||
var info = new MediaSourceInfo
|
||||
{
|
||||
Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
@@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
|
||||
MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
|
||||
Name = GetMediaSourceName(item),
|
||||
Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
|
||||
Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
|
||||
RunTimeTicks = item.RunTimeTicks,
|
||||
Container = item.Container,
|
||||
Size = item.Size,
|
||||
@@ -1610,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities
|
||||
return isAllowed;
|
||||
}
|
||||
|
||||
if (maxAllowedSubRating is not null)
|
||||
if (!maxAllowedRating.HasValue)
|
||||
{
|
||||
return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
|
||||
if (ratingScore.Score != maxAllowedRating.Value)
|
||||
{
|
||||
return ratingScore.Score < maxAllowedRating.Value;
|
||||
}
|
||||
|
||||
return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
|
||||
}
|
||||
|
||||
public ParentalRatingScore GetParentalRatingScore()
|
||||
@@ -2038,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities
|
||||
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
|
||||
await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that images within the item are still on the filesystem.
|
||||
/// </summary>
|
||||
|
||||
@@ -729,9 +729,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
query.StartIndex = startIndex;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
result.TotalRecordCount = totalCount;
|
||||
return result;
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
if (this is not UserRootFolder
|
||||
@@ -1001,9 +999,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
result.TotalRecordCount = totalItemCount;
|
||||
return result;
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
||||
@@ -1039,7 +1035,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
|
||||
}
|
||||
|
||||
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
|
||||
var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
|
||||
var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
|
||||
|
||||
if (query.EnableTotalRecordCount)
|
||||
{
|
||||
result.TotalRecordCount = filteredItems.Count;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
|
||||
@@ -1052,12 +1056,49 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
|
||||
if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
|
||||
{
|
||||
items = collectionManager.CollapseItemsWithinBoxSets(items, user);
|
||||
return items;
|
||||
}
|
||||
|
||||
return items;
|
||||
var config = configurationManager.Configuration;
|
||||
|
||||
bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
|
||||
bool collapseSeries = config.EnableGroupingShowsIntoCollections;
|
||||
|
||||
if (user is null || (collapseMovies && collapseSeries))
|
||||
{
|
||||
return collectionManager.CollapseItemsWithinBoxSets(items, user);
|
||||
}
|
||||
|
||||
if (!collapseMovies && !collapseSeries)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
var collapsibleItems = new List<BaseItem>();
|
||||
var remainingItems = new List<BaseItem>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
|
||||
{
|
||||
collapsibleItems.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
remainingItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (collapsibleItems.Count == 0)
|
||||
{
|
||||
return remainingItems;
|
||||
}
|
||||
|
||||
var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
|
||||
|
||||
return collapsedItems.Concat(remainingItems);
|
||||
}
|
||||
|
||||
private static bool CollapseBoxSetItems(
|
||||
@@ -1088,24 +1129,26 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
|
||||
var param = query.CollapseBoxSetItems;
|
||||
|
||||
if (!param.HasValue)
|
||||
if (param.HasValue)
|
||||
{
|
||||
if (user is not null && query.IncludeItemTypes.Any(type =>
|
||||
(type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) ||
|
||||
(type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0
|
||||
|| query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
|
||||
{
|
||||
param = true;
|
||||
}
|
||||
return param.Value && AllowBoxSetCollapsing(query);
|
||||
}
|
||||
|
||||
return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
|
||||
var config = configurationManager.Configuration;
|
||||
|
||||
bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie);
|
||||
bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series);
|
||||
|
||||
bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
|
||||
bool collapseSeries = config.EnableGroupingShowsIntoCollections;
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
|
||||
return canCollapse && AllowBoxSetCollapsing(query);
|
||||
}
|
||||
|
||||
return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
|
||||
}
|
||||
|
||||
private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
|
||||
@@ -1363,13 +1406,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
.Where(e => query is null || UserViewBuilder.FilterItem(e, query))
|
||||
.ToArray();
|
||||
|
||||
if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
|
||||
{
|
||||
realChildren = realChildren
|
||||
.OrderBy(e => e.ProductionYear ?? int.MaxValue)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var childCount = realChildren.Length;
|
||||
if (result.Count < limit)
|
||||
{
|
||||
|
||||
@@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public bool? UseRawName { get; set; }
|
||||
|
||||
public string? Person { get; set; }
|
||||
|
||||
public Guid[] PersonIds { get; set; }
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
|
||||
if (sortBy == ItemSortBy.Default)
|
||||
{
|
||||
return items;
|
||||
return items;
|
||||
}
|
||||
|
||||
return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
|
||||
@@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
|
||||
{
|
||||
var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query);
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||
{
|
||||
var children = base.GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
@@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Season };
|
||||
query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) };
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
|
||||
if (user is not null && !user.DisplayMissingEpisodes)
|
||||
{
|
||||
@@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -61,4 +62,108 @@ public static class FileSystemHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a single link hop for the specified path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT).
|
||||
/// </remarks>
|
||||
/// <param name="path">The file path to resolve.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
|
||||
/// </returns>
|
||||
private static FileInfo? Resolve(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Filesystem doesn't support links (e.g., exFAT).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target of the specified file link.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
|
||||
/// </remarks>
|
||||
/// <param name="linkPath">The path of the file link.</param>
|
||||
/// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
|
||||
/// </returns>
|
||||
public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
|
||||
{
|
||||
// Check if the file exists so the native resolve handler won't throw at us.
|
||||
if (!File.Exists(linkPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!returnFinalTarget)
|
||||
{
|
||||
return Resolve(linkPath);
|
||||
}
|
||||
|
||||
var targetInfo = Resolve(linkPath);
|
||||
if (targetInfo is null || !targetInfo.Exists)
|
||||
{
|
||||
return targetInfo;
|
||||
}
|
||||
|
||||
var currentPath = targetInfo.FullName;
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
|
||||
|
||||
while (true)
|
||||
{
|
||||
var linkInfo = Resolve(currentPath);
|
||||
if (linkInfo is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var targetPath = linkInfo.FullName;
|
||||
|
||||
// If an infinite loop is detected, return the file info for the
|
||||
// first link in the loop we encountered.
|
||||
if (!visited.Add(targetPath))
|
||||
{
|
||||
return new FileInfo(targetPath);
|
||||
}
|
||||
|
||||
targetInfo = linkInfo;
|
||||
currentPath = targetPath;
|
||||
|
||||
// Exit if the target doesn't exist, so the native resolve handler won't throw at us.
|
||||
if (!targetInfo.Exists)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return targetInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target of the specified file link.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
|
||||
/// </remarks>
|
||||
/// <param name="fileInfo">The file info of the file link.</param>
|
||||
/// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
|
||||
/// </returns>
|
||||
public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fileInfo);
|
||||
|
||||
return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <returns>Returns a Task that can be awaited.</returns>
|
||||
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches the user data to the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
|
||||
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the item.
|
||||
/// </summary>
|
||||
@@ -652,5 +660,12 @@ namespace MediaBrowser.Controller.Library
|
||||
/// This exists so plugins can trigger a library scan.
|
||||
/// </remarks>
|
||||
void QueueLibraryScan();
|
||||
|
||||
/// <summary>
|
||||
/// Add mblink file for a media path.
|
||||
/// </summary>
|
||||
/// <param name="virtualFolderPath">The path to the virtualfolder.</param>
|
||||
/// <param name="pathInfo">The new virtualfolder.</param>
|
||||
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -29,7 +30,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
/// </summary>
|
||||
private readonly Lock _taskLock = new();
|
||||
|
||||
private readonly BlockingCollection<TaskQueueItem> _tasks = new();
|
||||
private readonly Channel<TaskQueueItem> _tasks = Channel.CreateUnbounded<TaskQueueItem>();
|
||||
|
||||
private volatile int _workCounter;
|
||||
private Task? _cleanupTask;
|
||||
@@ -77,7 +78,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
|
||||
lock (_taskLock)
|
||||
{
|
||||
if (_tasks.Count > 0 || _workCounter > 0)
|
||||
if (_tasks.Reader.Count > 0 || _workCounter > 0)
|
||||
{
|
||||
_logger.LogDebug("Delay cleanup task, operations still running.");
|
||||
// tasks are still there so its still in use. Reschedule cleanup task.
|
||||
@@ -144,9 +145,9 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
_deadlockDetector.Value = stopToken.TaskStop;
|
||||
try
|
||||
{
|
||||
foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token))
|
||||
while (!stopToken.GlobalStop.Token.IsCancellationRequested)
|
||||
{
|
||||
stopToken.GlobalStop.Token.ThrowIfCancellationRequested();
|
||||
var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0;
|
||||
@@ -242,7 +243,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
if (ShouldForceSequentialOperation())
|
||||
if (ShouldForceSequentialOperation() || _deadlockDetector.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Process sequentially.");
|
||||
try
|
||||
@@ -264,35 +265,14 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
for (var i = 0; i < workItems.Length; i++)
|
||||
{
|
||||
var item = workItems[i]!;
|
||||
_tasks.Add(item, CancellationToken.None);
|
||||
await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_deadlockDetector.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Nested invocation detected, process in-place.");
|
||||
try
|
||||
{
|
||||
// we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved
|
||||
while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token))
|
||||
{
|
||||
await ProcessItem(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested)
|
||||
{
|
||||
// operation is cancelled. Do nothing.
|
||||
}
|
||||
|
||||
_logger.LogDebug("process in-place done.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Worker();
|
||||
_logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
|
||||
await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
|
||||
_logger.LogDebug("{NoWorkers} completed.", workItems.Length);
|
||||
ScheduleTaskCleanup();
|
||||
}
|
||||
Worker();
|
||||
_logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
|
||||
await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
|
||||
_logger.LogDebug("{NoWorkers} completed.", workItems.Length);
|
||||
ScheduleTaskCleanup();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -304,13 +284,12 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_tasks.CompleteAdding();
|
||||
_tasks.Writer.Complete();
|
||||
foreach (var item in _taskRunners)
|
||||
{
|
||||
await item.Key.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_tasks.Dispose();
|
||||
if (_cleanupTask is not null)
|
||||
{
|
||||
await _cleanupTask.ConfigureAwait(false);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -26,6 +26,7 @@ using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
@@ -157,13 +158,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
"wavpack"
|
||||
];
|
||||
|
||||
private readonly ILogger<EncodingHelper> _logger;
|
||||
|
||||
public EncodingHelper(
|
||||
IApplicationPaths appPaths,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IConfiguration config,
|
||||
IConfigurationManager configurationManager,
|
||||
IPathManager pathManager)
|
||||
IPathManager pathManager,
|
||||
ILogger<EncodingHelper> logger)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
@@ -171,6 +175,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
_config = config;
|
||||
_configurationManager = configurationManager;
|
||||
_pathManager = pathManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private enum DynamicHdrMetadataRemovalPlan
|
||||
@@ -2378,6 +2383,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// If SDR is the only supported range, we should not copy any of the HDR streams.
|
||||
// All the following copy check assumes at least one HDR format is supported.
|
||||
if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it.
|
||||
if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI)
|
||||
{
|
||||
@@ -3701,6 +3713,87 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the CRT-Lottes shader is requested via streamOptions and
|
||||
/// the compiled kernel file exists next to the server binary.
|
||||
/// </summary>
|
||||
public bool IsCrtShaderEnabled(EncodingJobInfo state)
|
||||
{
|
||||
var hasOption = state.BaseRequest.StreamOptions.TryGetValue("crtShader", out var val);
|
||||
var optionTrue = hasOption && string.Equals(val, "true", StringComparison.OrdinalIgnoreCase);
|
||||
var shaderPath = Path.Combine(AppContext.BaseDirectory, "Resources", "Shaders", "crt_lottes.cl");
|
||||
var fileExists = File.Exists(shaderPath);
|
||||
var hasOcl = _mediaEncoder.SupportsHwaccel("opencl");
|
||||
var hasProgramOcl = _mediaEncoder.SupportsFilter("program_opencl");
|
||||
var hasScaleOcl = _mediaEncoder.SupportsFilter("scale_opencl");
|
||||
|
||||
_logger.LogInformation(
|
||||
"CRT shader check: hasOption={HasOption} val={Val} fileExists={FileExists} path={Path} hasOcl={HasOcl} hasProgramOcl={HasProgramOcl} hasScaleOcl={HasScaleOcl}",
|
||||
hasOption, val, fileExists, shaderPath, hasOcl, hasProgramOcl, hasScaleOcl);
|
||||
|
||||
return optionTrue && fileExists && hasOcl && hasProgramOcl && hasScaleOcl;
|
||||
}
|
||||
|
||||
private string GetCrtEscapedShaderPath()
|
||||
{
|
||||
return Path.Combine(AppContext.BaseDirectory, "Resources", "Shaders", "crt_lottes.cl")
|
||||
.Replace("\\", "/", StringComparison.Ordinal)
|
||||
.Replace(":", "\\:", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string GetCrtBuildOpts(EncodingJobInfo state)
|
||||
{
|
||||
state.BaseRequest.StreamOptions.TryGetValue("crtShadowMask", out var maskVal);
|
||||
var shadowMask = int.TryParse(maskVal, out var m) && m >= 0 && m <= 4 ? m : 2;
|
||||
return FormattableString.Invariant($"-DSHADOW_MASK={shadowMask}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns three OpenCL-native filters (scale_opencl → program_opencl → scale_opencl)
|
||||
/// that apply the CRT shader while the frame stays in GPU VRAM.
|
||||
/// Use this when the pipeline is already in an OpenCL hardware context.
|
||||
/// Returns an empty list when the shader is disabled.
|
||||
/// </summary>
|
||||
public List<string> GetCrtShaderOclFilters(EncodingJobInfo state)
|
||||
{
|
||||
if (!IsCrtShaderEnabled(state))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var escapedPath = GetCrtEscapedShaderPath();
|
||||
var buildOpts = GetCrtBuildOpts(state);
|
||||
|
||||
// No scale_opencl format conversion needed: the shader reads and writes
|
||||
// NV12 planes directly (src_y, src_uv, dst_y, dst_uv).
|
||||
return
|
||||
[
|
||||
FormattableString.Invariant(
|
||||
$"program_opencl=source={escapedPath}:kernel=crt_lottes")
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FFmpeg filter chain that applies the CRT-Lottes OpenCL shader
|
||||
/// for the software (CPU-memory) pipeline path.
|
||||
/// Includes hwupload/hwdownload around the OpenCL kernel.
|
||||
/// Returns an empty string when the shader is disabled.
|
||||
/// </summary>
|
||||
public string GetCrtShaderFilter(EncodingJobInfo state)
|
||||
{
|
||||
if (!IsCrtShaderEnabled(state))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var escapedPath = GetCrtEscapedShaderPath();
|
||||
var buildOpts = GetCrtBuildOpts(state);
|
||||
|
||||
// Shader works with NV12 planes directly; no format conversion needed.
|
||||
return FormattableString.Invariant(
|
||||
$"hwupload=derive_device=opencl,program_opencl=source={escapedPath}:kernel=crt_lottes,hwdownload,format=nv12");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter of software filter chain.
|
||||
/// </summary>
|
||||
@@ -3822,6 +3915,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
|
||||
// CRT-Lottes OpenCL post-processing (applied after subtitle burn-in,
|
||||
// only when explicitly requested via streamOptions[crtShader]=true).
|
||||
var crtFilter = GetCrtShaderFilter(state);
|
||||
if (!string.IsNullOrEmpty(crtFilter))
|
||||
{
|
||||
mainFilters.Add(crtFilter);
|
||||
}
|
||||
|
||||
return (mainFilters, subFilters, overlayFilters);
|
||||
}
|
||||
|
||||
@@ -4808,6 +4909,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (doOclTonemap)
|
||||
{
|
||||
// CRT shader while already in OpenCL memory (after tonemap)
|
||||
if (IsCrtShaderEnabled(state))
|
||||
{
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
}
|
||||
|
||||
// OUTPUT qsv(nv12) surface(vram)
|
||||
// reverse-mapping via qsv(vaapi)-opencl interop.
|
||||
// add extra pool size to avoid the 'cannot allocate memory' error on hevc_qsv.
|
||||
@@ -4816,8 +4923,45 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (isVaapiDecoder)
|
||||
{
|
||||
mainFilters.Add("hwmap=derive_device=qsv");
|
||||
mainFilters.Add("format=qsv");
|
||||
if (IsCrtShaderEnabled(state))
|
||||
{
|
||||
// VAAPI → CPU → OpenCL (writable) → CRT (NV12) → CPU → QSV.
|
||||
// program_opencl inherits the hw frames context from its input link.
|
||||
// If that context was created via hwmap:mode=read, the output images are
|
||||
// CL_MEM_READ_ONLY and kernel writes are silently discarded (black output).
|
||||
// Solution: hwdownload+hwupload=derive_device=opencl creates a fresh
|
||||
// writable OpenCL context, matching Jellyfin's iHD doOclTonemap pattern.
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
mainFilters.Add("hwupload=derive_device=opencl");
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
mainFilters.Add("hwupload");
|
||||
mainFilters.Add("format=qsv");
|
||||
}
|
||||
else
|
||||
{
|
||||
// VAAPI → QSV (zero-copy, shared libva surface)
|
||||
mainFilters.Add("hwmap=derive_device=qsv");
|
||||
mainFilters.Add("format=qsv");
|
||||
}
|
||||
}
|
||||
else if (isQsvDecoder)
|
||||
{
|
||||
if (IsCrtShaderEnabled(state))
|
||||
{
|
||||
// QSV → CPU → OpenCL (writable) → CRT (NV12) → CPU → QSV.
|
||||
// Same reason as isVaapiDecoder: avoid read-only output pool.
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
mainFilters.Add("hwupload=derive_device=opencl");
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
mainFilters.Add("hwupload");
|
||||
mainFilters.Add("format=qsv");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5077,6 +5221,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
|
||||
mainFilters.Add(tonemapFilter);
|
||||
|
||||
// CRT shader inline — frame is already in OpenCL NV12, zero PCIe transfer.
|
||||
// scale_opencl converts NV12→RGBA for the kernel, then back to NV12.
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
}
|
||||
|
||||
if (doOclTonemap && isVaInVaOut)
|
||||
@@ -5086,6 +5234,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
|
||||
mainFilters.Add("format=vaapi");
|
||||
}
|
||||
else if (!doOclTonemap && isVaInVaOut && IsCrtShaderEnabled(state))
|
||||
{
|
||||
// CRT shader for pure-VAAPI path (no OCL tonemap already done).
|
||||
// VAAPI → OpenCL (GPU-internal via hwmap, no PCIe) → CRT → back to VAAPI.
|
||||
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
|
||||
mainFilters.Add("format=vaapi");
|
||||
}
|
||||
|
||||
var memoryOutput = false;
|
||||
var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
|
||||
@@ -5096,7 +5253,19 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
// prefer hwmap to hwdownload on opencl/vaapi.
|
||||
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
|
||||
if (isVaapiDecoder && isSwEncoder && !doOclTonemap && IsCrtShaderEnabled(state))
|
||||
{
|
||||
// Frame still in VAAPI (no prior OCL step). Route through OpenCL for CRT,
|
||||
// then download to CPU in one step (avoids double PCIe vs. SW-path CRT).
|
||||
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
mainFilters.Add("hwdownload");
|
||||
}
|
||||
else
|
||||
{
|
||||
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
|
||||
}
|
||||
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
@@ -5336,6 +5505,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// clear the surf->meta_offset and output nv12
|
||||
mainFilters.Add("scale_vaapi=format=nv12");
|
||||
|
||||
// CRT shader via VAAPI→OpenCL→VAAPI round-trip (all in GPU VRAM).
|
||||
if (IsCrtShaderEnabled(state))
|
||||
{
|
||||
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
|
||||
mainFilters.Add("format=vaapi");
|
||||
}
|
||||
|
||||
// hw deint
|
||||
if (doDeintH2645)
|
||||
{
|
||||
@@ -5349,7 +5527,19 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// OUTPUT nv12 surface(memory)
|
||||
if (isSwEncoder && (doVkTonemap || isVaapiDecoder))
|
||||
{
|
||||
mainFilters.Add("hwdownload");
|
||||
if (IsCrtShaderEnabled(state) && !doVkTonemap)
|
||||
{
|
||||
// Frame in VAAPI (no Vulkan tonemap done). Route through OpenCL for CRT
|
||||
// then download to CPU in one step.
|
||||
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
|
||||
mainFilters.AddRange(GetCrtShaderOclFilters(state));
|
||||
mainFilters.Add("hwdownload");
|
||||
}
|
||||
else
|
||||
{
|
||||
mainFilters.Add("hwdownload");
|
||||
}
|
||||
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
@@ -5942,28 +6132,37 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap;
|
||||
var swapOutputWandH = doRkVppTranspose && swapWAndH;
|
||||
var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts
|
||||
var outFormat = doOclTonemap ? "p010" : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH));
|
||||
|
||||
if (!hasSubs
|
||||
|| doRkVppTranspose
|
||||
|| !isFullAfbcPipeline
|
||||
|| !string.IsNullOrEmpty(doScaling))
|
||||
|| doScaling)
|
||||
{
|
||||
var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
|
||||
|
||||
// RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation,
|
||||
// but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it
|
||||
if (!string.IsNullOrEmpty(doScaling)
|
||||
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
||||
if (doScaling && !isScaleRatioSupported)
|
||||
{
|
||||
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
|
||||
// Use NV15 instead of P010 to avoid the issue.
|
||||
// SDR inputs are using BGRA formats already which is not affected.
|
||||
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
|
||||
var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat);
|
||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
// The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input.
|
||||
// Use 2pass here to enable RGA output of full-range YUV in the 2nd pass.
|
||||
if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling))
|
||||
{
|
||||
var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
|
||||
{
|
||||
hwScaleFilter += $":transpose={transposeDir}";
|
||||
@@ -6343,6 +6542,21 @@ 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))
|
||||
{
|
||||
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|
||||
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decoder = hardwareAccelerationType switch
|
||||
{
|
||||
HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
|
||||
@@ -7023,8 +7237,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
|
||||
// there's an issue about AV1 AFBC on RK3588, disable it for now until it's fixed upstream
|
||||
return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7090,6 +7304,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
|
||||
// Apply FFmpeg log level from configuration
|
||||
if (!string.IsNullOrEmpty(encodingOptions.FfmpegLogLevel))
|
||||
{
|
||||
inputModifier = $"-loglevel {encodingOptions.FfmpegLogLevel} -stats";
|
||||
}
|
||||
|
||||
var analyzeDurationArgument = string.Empty;
|
||||
|
||||
// Apply -analyzeduration as per the environment variable,
|
||||
|
||||
@@ -35,6 +35,14 @@ public interface IItemRepository
|
||||
|
||||
void SaveImages(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches the user data to the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
|
||||
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the item.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BDInfo.IO;
|
||||
@@ -58,6 +59,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHidden(ReadOnlySpan<char> name) => name.StartsWith('.');
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directories.
|
||||
/// </summary>
|
||||
@@ -65,6 +68,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
public IDirectoryInfo[] GetDirectories()
|
||||
{
|
||||
return _fileSystem.GetDirectories(_impl.FullName)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -76,6 +80,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
public IFileInfo[] GetFiles()
|
||||
{
|
||||
return _fileSystem.GetFiles(_impl.FullName)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -88,6 +93,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
public IFileInfo[] GetFiles(string searchPattern)
|
||||
{
|
||||
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -105,6 +111,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
new[] { searchPattern },
|
||||
false,
|
||||
searchOption == SearchOption.AllDirectories)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"transpose_opencl",
|
||||
"yadif_opencl",
|
||||
"bwdif_opencl",
|
||||
"program_opencl",
|
||||
// vaapi
|
||||
"scale_vaapi",
|
||||
"deinterlace_vaapi",
|
||||
|
||||
@@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
|
||||
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
|
||||
|
||||
if (!isAudio && _proberSupportsFirstVideoFrame)
|
||||
if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
|
||||
{
|
||||
args += " -show_frames -only_first_vframe";
|
||||
}
|
||||
|
||||
@@ -299,9 +299,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
// Handle WebM
|
||||
else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Limit WebM to supported codecs
|
||||
if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
|
||||
// Limit WebM to supported stream types and codecs.
|
||||
// FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible.
|
||||
// Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result.
|
||||
if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio)
|
||||
|| mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
splitFormat[i] = string.Empty;
|
||||
}
|
||||
@@ -853,7 +856,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
|
||||
if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
|
||||
if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio)
|
||||
&& string.IsNullOrEmpty(streamInfo.DisplayAspectRatio))
|
||||
{
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
|
||||
{
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||
ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
|
||||
|
||||
// If subtitles get burned in fonts may need to be extracted from the media file
|
||||
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
||||
if (state.SubtitleStream is not null && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode || state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding))
|
||||
{
|
||||
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ public class EncodingOptions
|
||||
EnableSubtitleExtraction = true;
|
||||
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"];
|
||||
HardwareDecodingCodecs = ["h264", "vc1"];
|
||||
FfmpegLogLevel = "error";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -295,4 +296,9 @@ public class EncodingOptions
|
||||
/// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.
|
||||
/// </summary>
|
||||
public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FFmpeg log level (quiet, panic, fatal, error, warning, info, verbose, debug, trace).
|
||||
/// </summary>
|
||||
public string FfmpegLogLevel { get; set; }
|
||||
}
|
||||
|
||||
@@ -1122,7 +1122,9 @@ public class StreamInfo
|
||||
|
||||
foreach (var pair in StreamOptions)
|
||||
{
|
||||
// Strip spaces to avoid having to encode h264 profile names
|
||||
// Strip spaces to avoid having to encode h264 profile names.
|
||||
// Use plain key=value (no streamOptions[] wrapper) so ParseStreamOptions
|
||||
// on the streaming endpoint picks them up correctly.
|
||||
sb.Append('&');
|
||||
sb.Append(pair.Key);
|
||||
sb.Append('=');
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Users
|
||||
{
|
||||
public enum ForgotPasswordAction
|
||||
{
|
||||
[Obsolete("Returning different actions represents a security concern.")]
|
||||
ContactAdmin = 0,
|
||||
PinCode = 1,
|
||||
[Obsolete("Returning different actions represents a security concern.")]
|
||||
InNetworkRequired = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,15 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
singular.AddRange(item.GetImages(ImageType.Backdrop));
|
||||
foreach (var backdrop in item.GetImages(ImageType.Backdrop))
|
||||
{
|
||||
var imageInMetadataFolder = backdrop.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled())
|
||||
{
|
||||
singular.Add(backdrop);
|
||||
}
|
||||
}
|
||||
|
||||
PruneImages(item, singular);
|
||||
|
||||
return singular.Count > 0;
|
||||
@@ -466,10 +474,36 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
if (UpdateMultiImages(item, images, ImageType.Backdrop))
|
||||
bool hasBackdrop = false;
|
||||
bool backdropStoredWithMedia = false;
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
changed = true;
|
||||
foundImageTypes.Add(ImageType.Backdrop);
|
||||
if (image.Type != ImageType.Backdrop)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasBackdrop = true;
|
||||
|
||||
if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
backdropStoredWithMedia = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBackdrop)
|
||||
{
|
||||
if (UpdateMultiImages(item, images, ImageType.Backdrop))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (backdropStoredWithMedia)
|
||||
{
|
||||
foundImageTypes.Add(ImageType.Backdrop);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundImageTypes.Count > 0)
|
||||
|
||||
@@ -151,7 +151,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
.ConfigureAwait(false);
|
||||
updateType |= beforeSaveResult;
|
||||
|
||||
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
|
||||
if (isFirstRefresh)
|
||||
{
|
||||
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Next run metadata providers
|
||||
if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
|
||||
@@ -229,6 +232,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
if (file is not null)
|
||||
{
|
||||
item.DateModified = file.LastWriteTimeUtc;
|
||||
|
||||
if (!file.IsDirectory)
|
||||
{
|
||||
item.Size = file.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
|
||||
await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return updateType;
|
||||
@@ -267,9 +275,14 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
|
||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken)
|
||||
{
|
||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||
if (reattachUserData)
|
||||
{
|
||||
await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (result.Item.SupportsPeople && result.People is not null)
|
||||
{
|
||||
var baseItem = result.Item;
|
||||
@@ -312,12 +325,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
|
||||
{
|
||||
if (isFullRefresh || updateType > ItemUpdateType.None)
|
||||
{
|
||||
var children = GetChildrenForMetadataUpdates(item);
|
||||
|
||||
updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
|
||||
}
|
||||
var children = GetChildrenForMetadataUpdates(item);
|
||||
updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
|
||||
}
|
||||
|
||||
var presentationUniqueKey = item.CreatePresentationUniqueKey();
|
||||
@@ -339,7 +348,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
item.DateModified = info.LastWriteTimeUtc;
|
||||
if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
|
||||
{
|
||||
item.DateCreated = info.CreationTimeUtc;
|
||||
if (info.CreationTimeUtc > DateTime.MinValue)
|
||||
{
|
||||
item.DateCreated = info.CreationTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
if (item is Video video)
|
||||
@@ -357,16 +369,24 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
{
|
||||
if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
|
||||
if (item is Folder folder)
|
||||
{
|
||||
if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren)
|
||||
if (!isFullRefresh && currentUpdateType == ItemUpdateType.None)
|
||||
{
|
||||
return true;
|
||||
return folder.SupportsDateLastMediaAdded;
|
||||
}
|
||||
|
||||
if (item is Folder folder)
|
||||
if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
|
||||
{
|
||||
return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks;
|
||||
if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,36 +407,42 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
|
||||
if (item is Folder folder)
|
||||
{
|
||||
updateType |= UpdateCumulativeRunTimeTicks(item, children);
|
||||
updateType |= UpdateDateLastMediaAdded(item, children);
|
||||
|
||||
// don't update user-changeable metadata for locked items
|
||||
if (item.IsLocked)
|
||||
if (folder.SupportsDateLastMediaAdded)
|
||||
{
|
||||
return updateType;
|
||||
updateType |= UpdateDateLastMediaAdded(item, children);
|
||||
}
|
||||
|
||||
if (EnableUpdatingPremiereDateFromChildren)
|
||||
if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks)
|
||||
{
|
||||
updateType |= UpdatePremiereDate(item, children);
|
||||
updateType |= UpdateCumulativeRunTimeTicks(item, children);
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableUpdatingGenresFromChildren)
|
||||
{
|
||||
updateType |= UpdateGenres(item, children);
|
||||
}
|
||||
if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked)
|
||||
{
|
||||
return updateType;
|
||||
}
|
||||
|
||||
if (EnableUpdatingStudiosFromChildren)
|
||||
{
|
||||
updateType |= UpdateStudios(item, children);
|
||||
}
|
||||
if (EnableUpdatingPremiereDateFromChildren)
|
||||
{
|
||||
updateType |= UpdatePremiereDate(item, children);
|
||||
}
|
||||
|
||||
if (EnableUpdatingOfficialRatingFromChildren)
|
||||
{
|
||||
updateType |= UpdateOfficialRating(item, children);
|
||||
}
|
||||
if (EnableUpdatingGenresFromChildren)
|
||||
{
|
||||
updateType |= UpdateGenres(item, children);
|
||||
}
|
||||
|
||||
if (EnableUpdatingStudiosFromChildren)
|
||||
{
|
||||
updateType |= UpdateStudios(item, children);
|
||||
}
|
||||
|
||||
if (EnableUpdatingOfficialRatingFromChildren)
|
||||
{
|
||||
updateType |= UpdateOfficialRating(item, children);
|
||||
}
|
||||
|
||||
return updateType;
|
||||
|
||||
@@ -721,8 +721,6 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_libraryManager.CreateItem(item, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -520,7 +520,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
Name = person.Name,
|
||||
Type = person.Type,
|
||||
Role = person.Role.Trim()
|
||||
Role = person.Role?.Trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
|
||||
}
|
||||
else
|
||||
{
|
||||
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
|
||||
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray();
|
||||
}
|
||||
|
||||
if (replaceData || targetItem.Shares.Count == 0)
|
||||
|
||||
@@ -303,9 +303,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
||||
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
|
||||
@@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
||||
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
|
||||
@@ -120,9 +120,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
||||
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
|
||||
@@ -367,9 +367,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
||||
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
|
||||
@@ -70,18 +70,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
public static PersonKind MapCrewToPersonType(Crew crew)
|
||||
{
|
||||
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
|
||||
&& crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
|
||||
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return PersonKind.Director;
|
||||
}
|
||||
|
||||
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
|
||||
&& crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
|
||||
&& crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return PersonKind.Producer;
|
||||
}
|
||||
|
||||
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
|
||||
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
|
||||
&& crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return PersonKind.Writer;
|
||||
}
|
||||
|
||||
@@ -68,12 +68,15 @@ namespace MediaBrowser.XbmcMetadata.Providers
|
||||
{
|
||||
var file = GetXmlFile(new ItemInfo(item), directoryService);
|
||||
|
||||
if (file is null)
|
||||
if (file?.Exists is not true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
|
||||
var fileTime = _fileSystem.GetLastWriteTimeUtc(file);
|
||||
|
||||
// 1 minute tolerance to avoid detecting our own file writes
|
||||
return (fileTime - item.DateLastSaved) > TimeSpan.FromMinutes(1);
|
||||
}
|
||||
|
||||
protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.12.0")]
|
||||
[assembly: AssemblyFileVersion("10.12.0")]
|
||||
[assembly: AssemblyVersion("10.11.6")]
|
||||
[assembly: AssemblyFileVersion("10.11.6")]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user