mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 03:53:01 +03:00
Compare commits
191 Commits
v10.11.0-r
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b46dda83b9 | ||
|
|
efabfbc931 | ||
|
|
6b5dc115e8 | ||
|
|
2dc0af667e | ||
|
|
55dbff8f30 | ||
|
|
2af43e0131 | ||
|
|
faf1cea63e | ||
|
|
7e25089c08 | ||
|
|
8fa36a38e2 | ||
|
|
5b3f29946b | ||
|
|
c869b5b884 | ||
|
|
a08b6ac266 | ||
|
|
4e68a5a078 | ||
|
|
99c68ddd50 | ||
|
|
d7f628677e | ||
|
|
e51680cf56 | ||
|
|
2e7d7752e9 | ||
|
|
26ac2ccd74 | ||
|
|
de9e653b73 | ||
|
|
e34e7a1d0b | ||
|
|
5a30f108fe | ||
|
|
74c9629372 | ||
|
|
6c5f448787 | ||
|
|
f848b8f12c | ||
|
|
bcec5f2e44 | ||
|
|
7d05c875f3 | ||
|
|
c805c5e2b1 | ||
|
|
c2c4c0adbf | ||
|
|
5ea3910af9 | ||
|
|
06fb300cff | ||
|
|
626ab7e00a | ||
|
|
1d140645b0 | ||
|
|
52f0c3dd24 | ||
|
|
b8327dbc9f | ||
|
|
d1722936c0 | ||
|
|
931240a3f5 | ||
|
|
b216a27bfc | ||
|
|
8471a67bcd | ||
|
|
b8a409195f | ||
|
|
1da67e5e10 | ||
|
|
ed1ec7ca6b | ||
|
|
3d7a68beb1 | ||
|
|
32fc57cf17 | ||
|
|
0598c6eaf6 | ||
|
|
0d7b687da0 | ||
|
|
e69754fd3a | ||
|
|
f693c9d39f | ||
|
|
96d72788a1 | ||
|
|
0d74a95bb8 | ||
|
|
a7d039b7c6 | ||
|
|
87b02b1316 | ||
|
|
871de372ff | ||
|
|
c9d93b0745 | ||
|
|
1ccd10863e | ||
|
|
4258df4485 | ||
|
|
63f06aad94 | ||
|
|
ffe82be7a7 | ||
|
|
23929a3e70 | ||
|
|
83d0dbdbcb | ||
|
|
573ce9ceaa | ||
|
|
f21fe9f95e | ||
|
|
f92eca3efb | ||
|
|
7d778d7bef | ||
|
|
21f65e2e27 | ||
|
|
28b0657608 | ||
|
|
a489942454 | ||
|
|
423c2654c0 | ||
|
|
4dc826644d | ||
|
|
0f21222a0c | ||
|
|
570b8b2eb9 | ||
|
|
08fd175f5a | ||
|
|
511b5d9c53 | ||
|
|
6514196e8d | ||
|
|
ed6cb30762 | ||
|
|
232c0399e2 | ||
|
|
dbb015441f | ||
|
|
4c1c160990 | ||
|
|
0931d6e4de | ||
|
|
3f2ebc4179 | ||
|
|
14e8194581 | ||
|
|
3c4dc16003 | ||
|
|
54d28d9842 | ||
|
|
adfa520057 | ||
|
|
5deb69b23f | ||
|
|
348b2992d7 | ||
|
|
9f8fb6d588 | ||
|
|
cee16d47cb | ||
|
|
9e53f46ad2 | ||
|
|
53dfcae1a6 | ||
|
|
efd659412f | ||
|
|
c31ea251c4 | ||
|
|
285e7c6c4f | ||
|
|
c274336563 | ||
|
|
d5fd5dfe6a | ||
|
|
6fa69f9fe5 | ||
|
|
0b876365a1 | ||
|
|
cdc8325c7b | ||
|
|
a6a8e29916 | ||
|
|
6fd3847298 | ||
|
|
3ff516a430 | ||
|
|
d8591840f3 | ||
|
|
c5affbbf71 | ||
|
|
788f090f27 | ||
|
|
0e3b6652b3 | ||
|
|
d167d59c23 | ||
|
|
f58b4860f7 | ||
|
|
96b7fc0ac0 | ||
|
|
c8ad861590 | ||
|
|
1a1a24cfff | ||
|
|
ace30afcf8 | ||
|
|
fc056b6273 | ||
|
|
ac5efb4775 | ||
|
|
fefd676adc | ||
|
|
59c17a663c | ||
|
|
641551e164 | ||
|
|
bd543d7ac3 | ||
|
|
545e412259 | ||
|
|
7dff92bb82 | ||
|
|
b36aab9399 | ||
|
|
2c7d2d4719 | ||
|
|
5c519270b8 | ||
|
|
55047b1183 | ||
|
|
794e1361d7 | ||
|
|
27c9c9c0ed | ||
|
|
68636b2390 | ||
|
|
2e6430c4f4 | ||
|
|
c88d792963 | ||
|
|
73dbc9e89f | ||
|
|
cf3edd9875 | ||
|
|
ef0131ad69 | ||
|
|
056c318f04 | ||
|
|
49c3443b0c | ||
|
|
e415718fe7 | ||
|
|
8abcfb2a80 | ||
|
|
9aadf97958 | ||
|
|
9e57121171 | ||
|
|
b471811920 | ||
|
|
3cb99add76 | ||
|
|
001f1c4377 | ||
|
|
9ef3706b44 | ||
|
|
864d6d0b8f | ||
|
|
a565e4896e | ||
|
|
ceef9143ad | ||
|
|
a7a92509c7 | ||
|
|
e876e784da | ||
|
|
9b7d5edc86 | ||
|
|
f01cddf273 | ||
|
|
0d4bd0495b | ||
|
|
6f9c4dea6e | ||
|
|
8c51920911 | ||
|
|
8f2fd65810 | ||
|
|
953659980f | ||
|
|
8ab1fecb70 | ||
|
|
f5d42ee180 | ||
|
|
e28d547006 | ||
|
|
b3b9f74014 | ||
|
|
07d31c6ba5 | ||
|
|
a9198e865e | ||
|
|
79ff0b0b00 | ||
|
|
2b45a984dd | ||
|
|
739642b330 | ||
|
|
6097045d71 | ||
|
|
51e20a14c2 | ||
|
|
eb0d05cf1e | ||
|
|
d3d5915f31 | ||
|
|
288640a5d0 | ||
|
|
ff0a1b999f | ||
|
|
da0fe7455e | ||
|
|
bf69f9d8a8 | ||
|
|
badf22fcc2 | ||
|
|
b59e9f90f0 | ||
|
|
056b92dbd5 | ||
|
|
ba80f5e416 | ||
|
|
97ec4c1da2 | ||
|
|
894ba1a410 | ||
|
|
0a0aaefad5 | ||
|
|
c8b97bf533 | ||
|
|
cfa4e357ea | ||
|
|
0f42aa892e | ||
|
|
cce6bf27e0 | ||
|
|
d6cebf1e67 | ||
|
|
c053a6cd78 | ||
|
|
d8c62420bf | ||
|
|
d483c3efe6 | ||
|
|
275c1a3cc1 | ||
|
|
4942b2c15f | ||
|
|
3fc71293b4 | ||
|
|
8ea9bece03 | ||
|
|
baa7f5f0b0 | ||
|
|
b9c96f3d2c | ||
|
|
08f9b932ac |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.9",
|
||||
"version": "9.0.11",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,4 +1,11 @@
|
||||
# Joshua must review all changes to deployment and build.sh
|
||||
.ci/* @joshuaboniface
|
||||
deployment/* @joshuaboniface
|
||||
build.sh @joshuaboniface
|
||||
# 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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/issue report.yml
vendored
2
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -87,7 +87,7 @@ body:
|
||||
label: Jellyfin Server version
|
||||
description: What version of Jellyfin are you using?
|
||||
options:
|
||||
- 10.10.0+
|
||||
- 10.11.0+
|
||||
- Master
|
||||
- Unstable
|
||||
- Older*
|
||||
|
||||
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
|
||||
20
.github/workflows/ci-compat.yml
vendored
20
.github/workflows/ci-compat.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: abi-head
|
||||
retention-days: 14
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
retention-days: 14
|
||||
@@ -85,13 +85,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download abi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: abi-head
|
||||
path: abi-head
|
||||
|
||||
- name: Download abi-base
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
path: abi-base
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
run: |
|
||||
{
|
||||
echo 'body<<EOF'
|
||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll Jellyfin.CodeAnalysis.dll; do
|
||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
||||
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
|
||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
body-includes: abi-diff-workflow-comment
|
||||
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
</details>
|
||||
|
||||
- name: Reply or edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
26
.github/workflows/ci-openapi.yml
vendored
26
.github/workflows/ci-openapi.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
@@ -80,12 +80,12 @@ jobs:
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
@@ -120,14 +120,14 @@ jobs:
|
||||
echo "" >> openapi-changes-reply.md
|
||||
echo "</details>" >> openapi-changes-reply.md
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
edit-mode: replace
|
||||
body-path: openapi-changes-reply.md
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
@@ -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@91f3272fc5907f4699dcf59761eb622a07342f5a # v1.2.3
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
@@ -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@91f3272fc5907f4699dcf59761eb622a07342f5a # v1.2.3
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
|
||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
with:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@1978db745da4a573ca4baa2d0f67175df51a148c # v5.4.16
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
8
.github/workflows/commands.yml
vendored
8
.github/workflows/commands.yml
vendored
@@ -17,14 +17,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -40,13 +40,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r rename/requirements.txt
|
||||
|
||||
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@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
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,13 +10,13 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r main-repo-triage/requirements.txt
|
||||
|
||||
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@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Custom Analyzers -->
|
||||
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' ">
|
||||
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' AND '$(Configuration)' == 'Debug' ">
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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.6" />
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
|
||||
<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.9" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" />
|
||||
<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.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<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" />
|
||||
@@ -68,7 +68,7 @@
|
||||
<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" />
|
||||
@@ -80,15 +80,15 @@
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.0.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
<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.9" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.9" />
|
||||
<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.5.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.8.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</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
|
||||
{
|
||||
[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>.*)$")]
|
||||
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>.*)$")]
|
||||
[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.StartsWith('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);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -224,7 +223,7 @@ public class ChapterManager : IChapterManager
|
||||
|
||||
if (saveChapters && changesMade)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
SaveChapters(video, chapters);
|
||||
}
|
||||
|
||||
DeleteDeadImages(currentImages, chapters);
|
||||
@@ -235,7 +234,9 @@ public class ChapterManager : IChapterManager
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
// Remove any chapters that are outside of the runtime of the video
|
||||
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
|
||||
_chapterRepository.SaveChapters(video.Id, validChapters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -251,23 +252,9 @@ public class ChapterManager : IChapterManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapterImages(Video video)
|
||||
public async Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = _pathManager.GetChapterImageFolderPath(video);
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id);
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex);
|
||||
}
|
||||
|
||||
_chapterRepository.DeleteChapters(video.Id);
|
||||
await _chapterRepository.DeleteChaptersAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
|
||||
|
||||
@@ -104,6 +104,8 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
|
||||
|
||||
_libraryManager.RootFolder.Children = null;
|
||||
|
||||
return FindFolders(path).First();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -152,6 +153,10 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public void MoveDirectory(string source, string destination)
|
||||
{
|
||||
// Make sure parent directory of target exists
|
||||
var parent = Directory.GetParent(destination);
|
||||
parent?.Create();
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(source, destination);
|
||||
@@ -248,47 +253,40 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
|
||||
// if (!result.IsDirectory)
|
||||
// {
|
||||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
// }
|
||||
|
||||
if (info is FileInfo fileInfo)
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
|
||||
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||
if (fileInfo.LinkTarget is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
|
||||
if (targetFileInfo is not null)
|
||||
{
|
||||
result.Length = RandomAccess.GetLength(fileHandle);
|
||||
result.Exists = targetFileInfo.Exists;
|
||||
if (result.Exists)
|
||||
{
|
||||
result.Length = targetFileInfo.Length;
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
// Dangling symlinks cannot be detected before opening the file unfortunately...
|
||||
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// IOException generally means the file is not accessible due to filesystem issues
|
||||
// Catch this exception and mark the file as not exist to ignore it
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
}
|
||||
}
|
||||
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
@@ -11,28 +12,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,60 +39,58 @@ 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.Attributes & FileAttributes.ReparsePoint) == 0
|
||||
&& 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)
|
||||
{
|
||||
using (var reader = dirIgnoreFile.OpenText())
|
||||
// If file has content, base ignoring off the content .gitignore-style rules
|
||||
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var ignore = new Ignore.Ignore();
|
||||
ignore.Add(rules);
|
||||
|
||||
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
|
||||
|
||||
// Add trailing slash for directories to match "folder/"
|
||||
if (isDirectory)
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
@@ -20,6 +21,7 @@ public class ExternalDataManager : IExternalDataManager
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly IChapterManager _chapterManager;
|
||||
private readonly ILogger<ExternalDataManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,18 +31,21 @@ public class ExternalDataManager : IExternalDataManager
|
||||
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||
/// <param name="chapterManager">The chapter manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ExternalDataManager(
|
||||
IKeyframeManager keyframeManager,
|
||||
IMediaSegmentManager mediaSegmentManager,
|
||||
IPathManager pathManager,
|
||||
ITrickplayManager trickplayManager,
|
||||
IChapterManager chapterManager,
|
||||
ILogger<ExternalDataManager> logger)
|
||||
{
|
||||
_keyframeManager = keyframeManager;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
_pathManager = pathManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
_chapterManager = chapterManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -67,5 +72,6 @@ public class ExternalDataManager : IExternalDataManager
|
||||
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _chapterManager.DeleteChapterDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -528,7 +534,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
Genre => _configurationManager.ApplicationPaths.GenrePath,
|
||||
MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
|
||||
MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
|
||||
MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
|
||||
Person => _configurationManager.ApplicationPaths.PeoplePath,
|
||||
Studio => _configurationManager.ApplicationPaths.StudioPath,
|
||||
Year => _configurationManager.ApplicationPaths.YearPath,
|
||||
@@ -1993,6 +1999,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)
|
||||
@@ -2129,6 +2141,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
item.ValidateImages();
|
||||
|
||||
_itemRepository.SaveImages(item);
|
||||
|
||||
RegisterItem(item);
|
||||
@@ -2148,6 +2162,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)
|
||||
@@ -3051,10 +3071,10 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
StartScanInBackground();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
|
||||
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -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 de reproducción: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
|
||||
"Latest": "Recientes",
|
||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
|
||||
"UserDownloadingItemWithValues": "{0} laeb alla {1}",
|
||||
"UserDownloadingItemWithValues": "{0} laadib alla {1}",
|
||||
"HeaderRecordingGroups": "Salvestusrühmad",
|
||||
"TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
|
||||
"TaskOptimizeDatabase": "Optimeeri andmebaasi",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
|
||||
"TaskDownloadMissingSubtitles": "Laadi alla puuduvad subtiitrid",
|
||||
"TaskDownloadMissingSubtitles": "Hangi puuduvad subtiitrid",
|
||||
"TaskRefreshChannelsDescription": "Värskendab veebikanalite teavet.",
|
||||
"TaskRefreshChannels": "Värskenda kanaleid",
|
||||
"TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkodeerimisfailid.",
|
||||
"TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkoodimisfailid.",
|
||||
"TaskCleanTranscode": "Puhasta transkoodimise kataloog",
|
||||
"TaskUpdatePluginsDescription": "Laadib alla ja paigaldab nende pluginate uuendused, mis on seadistatud automaatselt uuenduma.",
|
||||
"TaskUpdatePlugins": "Uuenda pluginaid",
|
||||
@@ -41,10 +41,10 @@
|
||||
"StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
|
||||
"User": "Kasutaja",
|
||||
"Undefined": "Määratlemata",
|
||||
"TvShows": "Seriaalid",
|
||||
"TvShows": "Sarjad",
|
||||
"System": "Süsteem",
|
||||
"Sync": "Sünkrooni",
|
||||
"Songs": "Laulud",
|
||||
"Songs": "Lood",
|
||||
"Shows": "Sarjad",
|
||||
"ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
|
||||
"ScheduledTaskFailedWithName": "{0} nurjus",
|
||||
@@ -92,7 +92,7 @@
|
||||
"HeaderNextUp": "Järgmisena",
|
||||
"HeaderLiveTV": "Otse TV",
|
||||
"HeaderFavoriteSongs": "Lemmiklood",
|
||||
"HeaderFavoriteShows": "Lemmikseriaalid",
|
||||
"HeaderFavoriteShows": "Lemmiksarjad",
|
||||
"HeaderFavoriteEpisodes": "Lemmikepisoodid",
|
||||
"HeaderFavoriteArtists": "Lemmikesitajad",
|
||||
"HeaderFavoriteAlbums": "Lemmikalbumid",
|
||||
@@ -122,18 +122,20 @@
|
||||
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
|
||||
"External": "Väline",
|
||||
"HearingImpaired": "Kuulmispuudega",
|
||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
|
||||
"TaskAudioNormalization": "Heli Normaliseerimine",
|
||||
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.",
|
||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadrid, et luua täpsemaid HLS-i esitusloendeid. See võib kesta pikka aega.",
|
||||
"TaskKeyframeExtractor": "Eralda võtmekaadrid",
|
||||
"TaskRefreshTrickplayImages": "Loo trickplay pildid",
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
|
||||
"TaskAudioNormalization": "Normaliseeri helitugevus",
|
||||
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
|
||||
"TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika",
|
||||
"TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika",
|
||||
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
|
||||
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
|
||||
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
|
||||
"TaskExtractMediaSegments": "Meediasegmentide skaneerimine",
|
||||
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
|
||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
||||
"TaskMoveTrickplayImages": "Migreeri trickplay piltide asukoht"
|
||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud."
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
{
|
||||
"Albums": "Álbumes",
|
||||
"Albums": "Álbums",
|
||||
"Collections": "Coleccións",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Channels": "Canles",
|
||||
"CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
|
||||
"CameraImageUploadedFrom": "Cargouse unha nova imaxe de cámara dende {0}",
|
||||
"Books": "Libros",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
|
||||
"Artists": "Artistas",
|
||||
"Application": "Aplicativo",
|
||||
"NotificationOptionServerRestartRequired": "Necesario un reinicio do servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización do Plugin instalada",
|
||||
"Application": "Aplicación",
|
||||
"NotificationOptionServerRestartRequired": "Necesario o reinicio do servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización do plugin instalada",
|
||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||
"NotificationOptionPluginError": "Fallo do Plugin",
|
||||
"NotificationOptionPluginError": "Fallo do plugin",
|
||||
"NotificationOptionNewLibraryContent": "Novo contido engadido",
|
||||
"NotificationOptionInstallationFailed": "Fallo na instalación",
|
||||
"NotificationOptionCameraImageUploaded": "Imaxe da cámara subida",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio parada",
|
||||
"NotificationOptionCameraImageUploaded": "Imaxe da cámara cargada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detida",
|
||||
"NotificationOptionAudioPlayback": "Reproducción de audio comezada",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualización da aplicación instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualización da aplicación dispoñible",
|
||||
"NewVersionIsAvailable": "Unha nova versión do Servidor Jellyfin está dispoñible para descarga.",
|
||||
"NewVersionIsAvailable": "Nova versión do Servidor Jellyfin dispoñible para descargar.",
|
||||
"NameSeasonUnknown": "Tempada descoñecida",
|
||||
"NameSeasonNumber": "Tempada {0}",
|
||||
"NameInstallFailed": "{0} instalación fallida",
|
||||
"MusicVideos": "Vídeos Musicais",
|
||||
"MusicVideos": "Vídeos musicais",
|
||||
"Music": "Música",
|
||||
"Movies": "Películas",
|
||||
"MixedContent": "Contido Mixto",
|
||||
"MessageServerConfigurationUpdated": "A configuración do servidor foi actualizada",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "A sección de configuración {0} do servidor foi actualizada",
|
||||
"MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado a {0}",
|
||||
"MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
|
||||
"MixedContent": "Contido mixto",
|
||||
"MessageServerConfigurationUpdated": "Actualizouse a configuración do servidor",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Actualizouse a sección de configuración {0} do servidor",
|
||||
"MessageApplicationUpdatedTo": "O servidor Jellyfin actualizouse a {0}",
|
||||
"MessageApplicationUpdated": "O servidor Jellyfin actualizouse",
|
||||
"Latest": "Último",
|
||||
"LabelRunningTimeValue": "Tempo de execución: {0}",
|
||||
"LabelRunningTimeValue": "Tempo en execución: {0}",
|
||||
"LabelIpAddressValue": "Enderezo IP: {0}",
|
||||
"ItemRemovedWithName": "{0} foi eliminado da biblioteca",
|
||||
"ItemAddedWithName": "{0} foi engadido a biblioteca",
|
||||
"ItemRemovedWithName": "{0} eliminouse da biblioteca",
|
||||
"ItemAddedWithName": "{0} engadiuse á biblioteca",
|
||||
"Inherit": "Herdar",
|
||||
"HomeVideos": "Videos caseiros",
|
||||
"HeaderRecordingGroups": "Grupos de Grabación",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"HeaderNextUp": "De seguido",
|
||||
"HeaderLiveTV": "TV en directo",
|
||||
"HeaderFavoriteSongs": "Cancións Favoritas",
|
||||
"HeaderFavoriteShows": "Series de TV Favoritas",
|
||||
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||
"HeaderFavoriteAlbums": "Álbunes Favoritos",
|
||||
"HeaderFavoriteSongs": "Cancións favoritas",
|
||||
"HeaderFavoriteShows": "Series de TV favoritas",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteAlbums": "Álbums favoritos",
|
||||
"HeaderContinueWatching": "Seguir vendo",
|
||||
"HeaderAlbumArtists": "Artistas do Album",
|
||||
"HeaderAlbumArtists": "Artistas do álbum",
|
||||
"Genres": "Xéneros",
|
||||
"Forced": "Forzado",
|
||||
"Folders": "Cartafoles",
|
||||
"Favorites": "Favoritos",
|
||||
"FailedLoginAttemptWithUserName": "Intento de incio de sesión fallido {0}",
|
||||
"FailedLoginAttemptWithUserName": "Fallo de intento de inicio de sesión dende {0}",
|
||||
"DeviceOnlineWithName": "{0} conectouse",
|
||||
"DeviceOfflineWithName": "{0} desconectouse",
|
||||
"Default": "Por defecto",
|
||||
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
|
||||
"TaskCleanLogs": "Limpar Carpeta de Rexistros",
|
||||
"TaskCleanActivityLog": "Limpar Rexistro de Actividade",
|
||||
"TasksChannelsCategory": "Canáis de Internet",
|
||||
"TaskUpdatePlugins": "Actualizar Plugins",
|
||||
"TaskCleanLogs": "Limpar directorio de rexistros",
|
||||
"TaskCleanActivityLog": "Limpar rexistro de actividade",
|
||||
"TasksChannelsCategory": "Canles da Internet",
|
||||
"TaskUpdatePlugins": "Actualizar plugins",
|
||||
"User": "Usuario",
|
||||
"Undefined": "Sen definir",
|
||||
"TvShows": "Programas de TV",
|
||||
"System": "Sistema",
|
||||
"Sync": "Sincronizar",
|
||||
"SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
|
||||
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está cargando. Por favor, ténteo axiña outra vez.",
|
||||
"Songs": "Cancións",
|
||||
"Shows": "Programas",
|
||||
"ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
|
||||
@@ -85,57 +85,57 @@
|
||||
"UserDeletedWithName": "O usuario {0} foi borrado",
|
||||
"UserCreatedWithName": "O usuario {0} foi creado",
|
||||
"Plugin": "Plugin",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detida",
|
||||
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
|
||||
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||
"NotificationOptionTaskFailed": "Falla na tarefa axendada",
|
||||
"TaskCleanTranscodeDescription": "Borra os arquivos de transcode anteriores a un día.",
|
||||
"TaskCleanTranscode": "Limpar Directorio de Transcode",
|
||||
"TaskCleanTranscodeDescription": "Borra os ficheiros de transcodificación de hai más dun día.",
|
||||
"TaskCleanTranscode": "Limpar o directorio de transcodificación",
|
||||
"UserStoppedPlayingItemWithValues": "{0} rematou de reproducir {1} en {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproducindo {1} en {2}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Busca en internet por subtítulos que faltan baseado na configuración de metadatos.",
|
||||
"UserStartedPlayingItemWithValues": "{0} está a reproducir {1} en {2}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procura na internet os subtítulos que faltan segundo a configuración de metadatos.",
|
||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos que faltan",
|
||||
"TaskRefreshChannelsDescription": "Refresca a información do canle de internet.",
|
||||
"TaskRefreshChannels": "Refrescar Canles",
|
||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizacións para plugins que están configurados para actualizarse automáticamente.",
|
||||
"TaskRefreshPeopleDescription": "Actualiza os metadatos dos actores e directores na túa libraría multimedia.",
|
||||
"TaskRefreshPeople": "Refrescar Persoas",
|
||||
"TaskCleanLogsDescription": "Borra arquivos de rexistro que son mais antigos que {0} días.",
|
||||
"TaskRefreshLibraryDescription": "Escanea a tua libraría multimedia buscando novos arquivos e refrescando os metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear Libraría Multimedia",
|
||||
"TaskRefreshChapterImagesDescription": "Crea previsualizacións para videos que teñen capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer Imaxes dos Capítulos",
|
||||
"TaskRefreshChannelsDescription": "Refresca a información da canle de internet.",
|
||||
"TaskRefreshChannels": "Refrescar canles",
|
||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizacións dos plugins configurados para actualizarse automáticamente.",
|
||||
"TaskRefreshPeopleDescription": "Actualiza os metadatos dos actores e directores na túa biblioteca de medios.",
|
||||
"TaskRefreshPeople": "Refrescar persoas",
|
||||
"TaskCleanLogsDescription": "Borra ficheiros de rexistro con máis de {0} días de antigüidade.",
|
||||
"TaskRefreshLibraryDescription": "Escanea a túa biblioteca de medios á procura de novos ficheiros e refresca os metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear a biblioteca de medios",
|
||||
"TaskRefreshChapterImagesDescription": "Crea miniaturas dos vídeos que teñen capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer imaxes dos capítulos",
|
||||
"TaskCleanCacheDescription": "Borra ficheiros da caché que xa non son necesarios para o sistema.",
|
||||
"TaskCleanCache": "Limpa Directorio de Caché",
|
||||
"TaskCleanActivityLogDescription": "Borra as entradas no rexistro de actividade anteriores á data configurada.",
|
||||
"TaskCleanCache": "Limpar directorio de caché",
|
||||
"TaskCleanActivityLogDescription": "Borra do rexistro de actividade as entradas anteriores á data configurada.",
|
||||
"TasksApplicationCategory": "Aplicación",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} foi engadido a túa libraría multimedia",
|
||||
"TasksLibraryCategory": "Libraría",
|
||||
"ValueHasBeenAddedToLibrary": "{0} engadiuse á túa biblioteca de medios",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Mantemento",
|
||||
"VersionNumber": "Versión {0}",
|
||||
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
|
||||
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
|
||||
"UserOnlineFromDevice": "{0} está en liña desde {1}",
|
||||
"UserOfflineFromDevice": "{0} desconectouse desde {1}",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
|
||||
"UserOfflineFromDevice": "{0} desconectouse dende {1}",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta e libera espazo na base de datos. Executar esta tarefa logo de facer cambios que muden a base de datos ou despois de escanear a biblioteca pode mellorar o rendemento.",
|
||||
"TaskOptimizeDatabase": "Optimizar base de datos",
|
||||
"TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
|
||||
"TaskKeyframeExtractorDescription": "Extrae fotogramas clave dos vídeos para crear listas de reprodución HLS máis precisas. Podería levar moito tempo.",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Problemas de audición",
|
||||
"TaskKeyframeExtractor": "Extractor de fragmentos",
|
||||
"TaskAudioNormalization": "Normalización do audio",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogramas clave",
|
||||
"TaskAudioNormalization": "Normalización de volume",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
|
||||
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
|
||||
"TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos multimedia",
|
||||
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (Estado de visualización, estado de favorito, etc) da multimedia que leve non presente polo menos durante 90 días."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita ítems que xa non existen das coleccións e listas de reprodución.",
|
||||
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",
|
||||
"TaskMoveTrickplayImagesDescription": "Move as miniaturas de previsualización segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar miniaturas de previsualización",
|
||||
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Sammlungen",
|
||||
"DeviceOfflineWithName": "{0} wurde getrennt",
|
||||
"DeviceOnlineWithName": "{0} ist verbunden",
|
||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
||||
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
|
||||
"Favorites": "Favorite",
|
||||
"Folders": "Ordner",
|
||||
"Genres": "Genre",
|
||||
|
||||
@@ -129,5 +129,12 @@
|
||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
|
||||
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
|
||||
"HearingImpaired": "Oštećen sluh",
|
||||
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
|
||||
"TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
|
||||
"TaskRefreshTrickplayImages": "Generiraj slike brzog pregledavanja",
|
||||
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
|
||||
"TaskAudioNormalization": "Normalizacija zvuka",
|
||||
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
|
||||
@@ -135,8 +135,8 @@
|
||||
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
|
||||
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
|
||||
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
|
||||
"TaskMoveTrickplayImages": "Preseli lokaciju Trickplay slika",
|
||||
"TaskMoveTrickplayImagesDescription": "Preseli lokaciju Trickplay slika prema postavkama zbirke.",
|
||||
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
|
||||
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.",
|
||||
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
|
||||
}
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
{
|
||||
"Books": "liv"
|
||||
"Books": "Liv",
|
||||
"TasksLibraryCategory": "Libreri",
|
||||
"Albums": "Albòm yo",
|
||||
"Artists": "Atis yo",
|
||||
"Application": "Aplikasyon",
|
||||
"Channels": "Kanal yo",
|
||||
"ChapterNameValue": "Chapit {0}",
|
||||
"Default": "Defo",
|
||||
"DeviceOnlineWithName": "{0} konekte",
|
||||
"DeviceOfflineWithName": "{0} dekonekte",
|
||||
"External": "Extèn",
|
||||
"Collections": "Koleksyon yo",
|
||||
"Favorites": "Pi Renmen",
|
||||
"Folders": "Dosye",
|
||||
"Genres": "Jan yo",
|
||||
"Forced": "Fòse",
|
||||
"HeaderAlbumArtists": "Albòm Atis",
|
||||
"HeaderContinueWatching": "Kontinye Kade",
|
||||
"HeaderFavoriteAlbums": "Albòm Pi Renmen",
|
||||
"HeaderFavoriteArtists": "Atis Pi Renmen",
|
||||
"HeaderFavoriteEpisodes": "Epizòd Pi Renmen",
|
||||
"HeaderFavoriteShows": "Emisyon Pi Renmen",
|
||||
"HeaderFavoriteSongs": "Mizik Pi Renmen",
|
||||
"HeaderLiveTV": "Televizyon an Direk",
|
||||
"HeaderNextUp": "Pwochen an",
|
||||
"HomeVideos": "Videyo Lakay",
|
||||
"Latest": "Pi Resan",
|
||||
"MessageApplicationUpdated": "Sèvè Jellyfin met a jou",
|
||||
"MessageApplicationUpdatedTo": "Sèvè Jellyfin met a jou sou {0}",
|
||||
"Movies": "Fim",
|
||||
"MixedContent": "Kontni Melanje",
|
||||
"Music": "Mizik",
|
||||
"MusicVideos": "Videyo Mizik",
|
||||
"NameInstallFailed": "{0} enstalasyon fe fayit",
|
||||
"NameSeasonNumber": "Sezon {0}",
|
||||
"NameSeasonUnknown": "Sezon Enkoni",
|
||||
"NotificationOptionCameraImageUploaded": "Imaj Kamera telechaje",
|
||||
"NotificationOptionInstallationFailed": "Enstalasyon echwe",
|
||||
"Photos": "Foto",
|
||||
"PluginInstalledWithName": "{0} te enstale",
|
||||
"PluginUninstalledWithName": "{0} te dezenstale",
|
||||
"PluginUpdatedWithName": "{0} te mi a jou",
|
||||
"ScheduledTaskFailedWithName": "{0} echwe",
|
||||
"ScheduledTaskStartedWithName": "{0} komanse",
|
||||
"Songs": "Mizik yo",
|
||||
"Shows": "Emisyon yo",
|
||||
"System": "Sistèm",
|
||||
"TvShows": "Emisyon Tele",
|
||||
"User": "Itilizatè",
|
||||
"UserCreatedWithName": "Itilizatè {0} kreye",
|
||||
"UserDeletedWithName": "Itilizatè {0} a efase",
|
||||
"UserDownloadingItemWithValues": "{0} ap telechaje {1}",
|
||||
"UserOfflineFromDevice": "{0} dekonekte de {1}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ap jwe {1} sou {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} fin jwe {1} sou {2}",
|
||||
"UserPasswordChangedWithName": "Modpas la chanje pou Itilizatè {0}",
|
||||
"ValueSpecialEpisodeName": "Spesyal - {0}",
|
||||
"VersionNumber": "Vesyon {0}",
|
||||
"TasksApplicationCategory": "Aplikasyon",
|
||||
"TasksMaintenanceCategory": "Antretyen"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
||||
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
|
||||
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
|
||||
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"Books": "Номнууд",
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Playlists": "Тоглуулах жагсаалтууд",
|
||||
"Movies": "Кинонууд",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрлүүд",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Collections": "Цуглуулгууд",
|
||||
"Artists": "Уран бүтээлчид",
|
||||
"Albums": "Цомгууд",
|
||||
"Albums": "Дуут цомгууд",
|
||||
"TaskExtractMediaSegments": "Медиа сегмент шалга",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
|
||||
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
|
||||
@@ -63,15 +63,15 @@
|
||||
"CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
|
||||
"Channels": "Сувгууд",
|
||||
"ChapterNameValue": "{0}-р бүлэг",
|
||||
"Default": "Өгөгдмөл",
|
||||
"Default": "Анхдагч",
|
||||
"DeviceOfflineWithName": "{0}-н холболт саллаа",
|
||||
"DeviceOnlineWithName": "{0} холбогдлоо",
|
||||
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
|
||||
"Folders": "Хавтаснууд",
|
||||
"Folders": "Хавтасууд",
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderLiveTV": "Шууд ТВ",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
@@ -84,8 +84,8 @@
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
|
||||
"MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
|
||||
"MixedContent": "Холимог агуулга",
|
||||
"Music": "Дуу",
|
||||
"MusicVideos": "Дууны клип",
|
||||
"Music": "Хөгжим",
|
||||
"MusicVideos": "Дууны клипүүд",
|
||||
"NameInstallFailed": "{0} суулгахад алдаа гарлаа",
|
||||
"NameSeasonNumber": "{0}-р улирал",
|
||||
"NameSeasonUnknown": "Улирал олдсонгүй",
|
||||
@@ -101,15 +101,15 @@
|
||||
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
|
||||
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
|
||||
"Photos": "Зургууд",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin": "Плагин",
|
||||
"PluginInstalledWithName": "{0}-г суулгалаа",
|
||||
"PluginUninstalledWithName": "{0}-г устгалаа",
|
||||
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
|
||||
"ProviderValue": "Нийлүүлэгч: {0}",
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Нэвтрүүлгүүд",
|
||||
"Sync": "Дахин",
|
||||
"Shows": "Шоу",
|
||||
"Sync": "Синхрончлох",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
@@ -122,7 +122,7 @@
|
||||
"UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
|
||||
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
|
||||
"ValueSpecialEpisodeName": "Тусгай - {0}",
|
||||
"ValueSpecialEpisodeName": "Онцгой - {0}",
|
||||
"VersionNumber": "Хувилбар {0}",
|
||||
"TasksMaintenanceCategory": "Засвар",
|
||||
"TasksLibraryCategory": "Сан",
|
||||
|
||||
@@ -132,5 +132,10 @@
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
|
||||
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
|
||||
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
|
||||
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
|
||||
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
|
||||
}
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
||||
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
||||
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
||||
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
|
||||
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
|
||||
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
|
||||
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"Collections": "Barrels",
|
||||
"ItemAddedWithName": "{0} is now with yer treasure",
|
||||
"Default": "Normal-like",
|
||||
"FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
|
||||
"FailedLoginAttemptWithUserName": "Ye failed to enter from {0}",
|
||||
"Favorites": "Finest Loot",
|
||||
"ItemRemovedWithName": "{0} was taken from yer treasure",
|
||||
"LabelIpAddressValue": "Ship's coordinates: {0}",
|
||||
@@ -113,5 +113,10 @@
|
||||
"TaskCleanCache": "Sweep the Cache Chest",
|
||||
"TaskRefreshChapterImages": "Claim chapter portraits",
|
||||
"TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.",
|
||||
"TaskRefreshLibrary": "Scan the Treasure Trove"
|
||||
"TaskRefreshLibrary": "Scan the Treasure Trove",
|
||||
"TasksChannelsCategory": "Channels o' thy Internet",
|
||||
"TaskRefreshTrickplayImages": "Summon the picture tricks",
|
||||
"TaskRefreshTrickplayImagesDescription": "Summons picture trick previews for videos in ye enabled book roost",
|
||||
"TaskUpdatePlugins": "Resummon yer Plugins",
|
||||
"TaskCleanTranscode": "Swab Ye Transcode Directory"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Artists": "Artistas",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||
"Books": "Livros",
|
||||
"CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
|
||||
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
|
||||
"Channels": "Canais",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Coleções",
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
{
|
||||
"Books": "کتابیں"
|
||||
"Books": "کتابیں",
|
||||
"AppDeviceValues": "ایپ: {0}، ڈیوائس: {1}",
|
||||
"Albums": "البمز",
|
||||
"Application": "ایپلی کیشن",
|
||||
"Artists": "فنکار",
|
||||
"AuthenticationSucceededWithUserName": "{0} کی کامیابی سے تصدیق ہو چکی ہے",
|
||||
"CameraImageUploadedFrom": "ایک نئی کیمرے کی تصویر {0} سے اپ لوڈ کی گئی ہے",
|
||||
"Channels": "چینلز",
|
||||
"ChapterNameValue": "باب {0}",
|
||||
"Collections": "مجموعے",
|
||||
"Default": "ڈیفالٹ",
|
||||
"DeviceOfflineWithName": "{0} نے رابطہ منقطع کر دیا ہے",
|
||||
"DeviceOnlineWithName": "{0} منسلک ہے",
|
||||
"External": "بیرونی"
|
||||
}
|
||||
|
||||
@@ -123,5 +123,9 @@
|
||||
"TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔",
|
||||
"External": "بیرونی",
|
||||
"HearingImpaired": "قوت سماعت سے محروم",
|
||||
"TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں"
|
||||
"TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں",
|
||||
"TaskDownloadMissingLyrics": "غائب بول ڈاؤن لوڈ کریں",
|
||||
"TaskDownloadMissingLyricsDescription": "گانے کے غائب بول ڈاؤن لوڈ کریں",
|
||||
"TaskAudioNormalization": "آڈیو نارملائزیشن",
|
||||
"TaskAudioNormalizationDescription": "آڈیو نارملائزیشن ڈیٹا کے لیے فائلوں کو سکین کرتا ہے۔"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"TasksMaintenanceCategory": "Bảo Trì",
|
||||
"VersionNumber": "Phiên Bản {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
|
||||
"UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
|
||||
"UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
|
||||
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteShows": "最愛的節目",
|
||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||
"HeaderLiveTV": "電視直播",
|
||||
"HeaderNextUp": "接著播放",
|
||||
"HeaderNextUp": "繼續觀看",
|
||||
"HeaderRecordingGroups": "錄製組",
|
||||
"HomeVideos": "家庭影片",
|
||||
"Inherit": "繼承",
|
||||
@@ -127,8 +127,8 @@
|
||||
"HearingImpaired": "聽力障礙",
|
||||
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
|
||||
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
|
||||
"TaskExtractMediaSegments": "掃描媒體段落",
|
||||
"TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段落。",
|
||||
"TaskExtractMediaSegments": "掃描媒體分段資訊",
|
||||
"TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。",
|
||||
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
|
||||
"TaskDownloadMissingLyricsDescription": "下載歌詞",
|
||||
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
|
||||
@@ -137,5 +137,6 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||
"CleanupUserDataTask": "用戶資料清理工作"
|
||||
"CleanupUserDataTask": "用戶資料清理工作",
|
||||
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -261,14 +261,22 @@ public partial class AudioNormalizationTask : IScheduledTask
|
||||
|
||||
using var reader = process.StandardError;
|
||||
float? lufs = null;
|
||||
var foundLufs = false;
|
||||
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (match.Success)
|
||||
if (foundLufs)
|
||||
{
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
foundLufs = true;
|
||||
}
|
||||
|
||||
if (lufs is null)
|
||||
|
||||
@@ -223,15 +223,14 @@ namespace Emby.Server.Implementations.Updates
|
||||
Guid id = default,
|
||||
Version? specificVersion = null)
|
||||
{
|
||||
if (name is not null)
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!id.IsEmpty())
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Id.Equals(id));
|
||||
}
|
||||
else if (name is not null)
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (specificVersion is not null)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -779,12 +779,14 @@ public class LibraryController : BaseJellyfinApiController
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Genres = item.Genres,
|
||||
Tags = item.Tags,
|
||||
Limit = limit,
|
||||
IncludeItemTypes = includeItemTypes.ToArray(),
|
||||
DtoOptions = dtoOptions,
|
||||
EnableTotalRecordCount = !isMovie ?? true,
|
||||
EnableGroupByMetadataKey = isMovie ?? false,
|
||||
ExcludeItemIds = [itemId]
|
||||
ExcludeItemIds = [itemId],
|
||||
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
|
||||
};
|
||||
|
||||
// ExcludeArtistIds
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Jellyfin.Api.Formatters;
|
||||
@@ -6,7 +10,7 @@ namespace Jellyfin.Api.Formatters;
|
||||
/// <summary>
|
||||
/// Xml output formatter.
|
||||
/// </summary>
|
||||
public sealed class XmlOutputFormatter : StringOutputFormatter
|
||||
public sealed class XmlOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
|
||||
@@ -15,5 +19,24 @@ public sealed class XmlOutputFormatter : StringOutputFormatter
|
||||
{
|
||||
SupportedMediaTypes.Clear();
|
||||
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
SupportedEncodings.Add(Encoding.Unicode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(selectedEncoding);
|
||||
|
||||
var valueAsString = context.Object?.ToString();
|
||||
if (string.IsNullOrEmpty(valueAsString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
await response.WriteAsync(valueAsString, selectedEncoding).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -128,7 +128,8 @@ public class BackupService : IBackupService
|
||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||
|
||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
||||
|| Path.EndsInDirectorySeparator(item.FullName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -199,7 +200,7 @@ public class BackupService : IBackupService
|
||||
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
|
||||
if (zipEntry is null)
|
||||
{
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -223,7 +224,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
|
||||
_logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,11 +234,11 @@ public class BackupService : IBackupService
|
||||
|
||||
_logger.LogInformation("Try restore Database");
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("Restored database.");
|
||||
_logger.LogInformation("Restored database");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +264,8 @@ public class BackupService : IBackupService
|
||||
Options = Map(backupOptions)
|
||||
};
|
||||
|
||||
_logger.LogInformation("Running database optimization before backup");
|
||||
|
||||
await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var backupFolder = Path.Combine(_applicationPaths.BackupPath);
|
||||
@@ -281,130 +284,154 @@ public class BackupService : IBackupService
|
||||
}
|
||||
|
||||
var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
|
||||
_logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
|
||||
var fileStream = File.OpenWrite(backupPath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Start backup process.");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
_logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
|
||||
var fileStream = File.OpenWrite(backupPath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
{
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
_logger.LogInformation("Starting backup process");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
return (IAsyncEnumerable<object>)enumerable;
|
||||
}
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
// include the migration history as well
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Begin Database backup");
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
{
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
return (IAsyncEnumerable<object>)enumerable;
|
||||
}
|
||||
|
||||
// include the migration history as well
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
|
||||
[
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Begin Database backup");
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||
await using (jsonSerializer.ConfigureAwait(false))
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
{
|
||||
jsonSerializer.WriteStartArray();
|
||||
|
||||
var set = entityType.ValueFactory().ConfigureAwait(false);
|
||||
await foreach (var item in set.ConfigureAwait(false))
|
||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||
await using (jsonSerializer.ConfigureAwait(false))
|
||||
{
|
||||
entities++;
|
||||
try
|
||||
jsonSerializer.WriteStartArray();
|
||||
|
||||
var set = entityType.ValueFactory().ConfigureAwait(false);
|
||||
await foreach (var item in set.ConfigureAwait(false))
|
||||
{
|
||||
JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load entity {Entity}", item);
|
||||
throw;
|
||||
entities++;
|
||||
try
|
||||
{
|
||||
using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings);
|
||||
document.WriteTo(jsonSerializer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load entity {Entity}", item);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
jsonSerializer.WriteEndArray();
|
||||
}
|
||||
|
||||
jsonSerializer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities);
|
||||
_logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
{
|
||||
if (!Directory.Exists(source))
|
||||
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
return;
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", source);
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
if (!Directory.Exists(source))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", source);
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
|
||||
if (backupOptions.Subtitles)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
|
||||
}
|
||||
|
||||
if (backupOptions.Trickplay)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
|
||||
}
|
||||
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
await using (manifestStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
|
||||
if (backupOptions.Subtitles)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
|
||||
}
|
||||
|
||||
if (backupOptions.Trickplay)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
|
||||
}
|
||||
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
await using (manifestStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||
}
|
||||
_logger.LogInformation("Backup created");
|
||||
return Map(manifest, backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
|
||||
try
|
||||
{
|
||||
if (File.Exists(backupPath))
|
||||
{
|
||||
File.Delete(backupPath);
|
||||
}
|
||||
}
|
||||
catch (Exception innerEx)
|
||||
{
|
||||
_logger.LogWarning(innerEx, "Unable to remove failed backup");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup created");
|
||||
return Map(manifest, backupPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -422,7 +449,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath);
|
||||
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -459,7 +486,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load {BackupArchive} path.", item);
|
||||
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ public sealed class BaseItemRepository
|
||||
private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
|
||||
private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
|
||||
private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
|
||||
private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
|
||||
@@ -266,13 +267,15 @@ public sealed class BaseItemRepository
|
||||
IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
|
||||
|
||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
|
||||
if (filter.EnableTotalRecordCount)
|
||||
{
|
||||
result.TotalRecordCount = dbQuery.Count();
|
||||
}
|
||||
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
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;
|
||||
@@ -292,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();
|
||||
}
|
||||
@@ -335,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();
|
||||
}
|
||||
|
||||
@@ -397,9 +403,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = dbQuery.Distinct();
|
||||
}
|
||||
|
||||
dbQuery = ApplyOrder(dbQuery, filter);
|
||||
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
dbQuery = ApplyOrder(dbQuery, filter, context);
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
@@ -444,6 +448,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;
|
||||
}
|
||||
|
||||
@@ -612,6 +617,13 @@ public sealed class BaseItemRepository
|
||||
else
|
||||
{
|
||||
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
|
||||
if (entity.Images is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemImageInfos.AddRange(entity.Images);
|
||||
}
|
||||
|
||||
context.BaseItems.Attach(entity).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
@@ -1230,8 +1242,20 @@ public sealed class BaseItemRepository
|
||||
ExcludeItemIds = filter.ExcludeItemIds
|
||||
};
|
||||
|
||||
var query = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||
.GroupBy(e => e.PresentationUniqueKey);
|
||||
var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||
.GroupBy(e => e.PresentationUniqueKey)
|
||||
.Select(e => e.FirstOrDefault())
|
||||
.Select(e => e!.Id);
|
||||
|
||||
var query = context.BaseItems
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery()
|
||||
.Where(e => masterQuery.Contains(e.Id));
|
||||
|
||||
query = ApplyOrder(query, filter, context);
|
||||
|
||||
var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
|
||||
if (filter.EnableTotalRecordCount)
|
||||
@@ -1286,12 +1310,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var resultQuery = query.Select(e => new
|
||||
{
|
||||
item = e.AsQueryable()
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery().First(),
|
||||
item = e,
|
||||
// TODO: This is bad refactor!
|
||||
itemCount = new ItemCounts()
|
||||
{
|
||||
@@ -1323,13 +1342,6 @@ public sealed class BaseItemRepository
|
||||
result.Items =
|
||||
[
|
||||
.. query
|
||||
.Select(e => e.AsQueryable()
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery()
|
||||
.First())
|
||||
.AsEnumerable()
|
||||
.Where(e => e is not null)
|
||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||
@@ -1509,7 +1521,7 @@ 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 hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
|
||||
@@ -1528,7 +1540,7 @@ public sealed class BaseItemRepository
|
||||
var firstOrdering = orderBy.FirstOrDefault();
|
||||
if (firstOrdering != default)
|
||||
{
|
||||
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
|
||||
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
|
||||
if (firstOrdering.SortOrder == SortOrder.Ascending)
|
||||
{
|
||||
orderedQuery = query.OrderBy(expression);
|
||||
@@ -1553,7 +1565,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
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);
|
||||
@@ -1692,8 +1704,17 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrEmpty(filter.SearchTerm))
|
||||
{
|
||||
var searchTerm = filter.SearchTerm.ToLower();
|
||||
baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
|
||||
var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
|
||||
var originalSearchTerm = filter.SearchTerm.ToLower();
|
||||
if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
|
||||
{
|
||||
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!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm)));
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.IsFolder.HasValue)
|
||||
@@ -1746,7 +1767,8 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Path))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Path == filter.Path);
|
||||
var pathToQuery = GetPathToSave(filter.Path);
|
||||
baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
|
||||
@@ -1865,10 +1887,17 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.PersonIds.Length > 0)
|
||||
{
|
||||
var peopleEntityIds = context.BaseItems
|
||||
.WhereOneOrMany(filter.PersonIds, b => b.Id)
|
||||
.Join(
|
||||
context.Peoples,
|
||||
b => b.Name,
|
||||
p => p.Name,
|
||||
(b, p) => p.Id);
|
||||
|
||||
baseQuery = baseQuery
|
||||
.Where(e =>
|
||||
context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name))
|
||||
.Any(f => f.ItemId == e.Id));
|
||||
.Where(e => context.PeopleBaseItemMap
|
||||
.Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Person))
|
||||
@@ -1904,26 +1933,35 @@ public sealed class BaseItemRepository
|
||||
var nameContains = filter.NameContains;
|
||||
if (!string.IsNullOrWhiteSpace(nameContains))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.CleanName!.Contains(nameContains)
|
||||
|| e.OriginalTitle!.ToLower().Contains(nameContains!));
|
||||
if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
|
||||
{
|
||||
nameContains = $"%{nameContains.Trim('%')}%";
|
||||
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.OriginalTitle, nameContains));
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.CleanName!.Contains(nameContains)
|
||||
|| e.OriginalTitle!.ToLower().Contains(nameContains!));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1989,7 +2027,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
|
||||
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ArtistIds);
|
||||
}
|
||||
|
||||
if (filter.AlbumArtistIds.Length > 0)
|
||||
@@ -1999,7 +2037,18 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ContributingArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
|
||||
var contributingNames = context.BaseItems
|
||||
.Where(b => filter.ContributingArtistIds.Contains(b.Id))
|
||||
.Select(b => b.CleanName);
|
||||
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(ivm =>
|
||||
ivm.ItemValue.Type == ItemValueType.Artist &&
|
||||
contributingNames.Contains(ivm.ItemValue.CleanValue))
|
||||
&&
|
||||
!e.ItemValues!.Any(ivm =>
|
||||
ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
|
||||
contributingNames.Contains(ivm.ItemValue.CleanValue)));
|
||||
}
|
||||
|
||||
if (filter.AlbumIds.Length > 0)
|
||||
@@ -2010,7 +2059,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ExcludeArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
|
||||
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
|
||||
}
|
||||
|
||||
if (filter.GenreIds.Count > 0)
|
||||
@@ -2054,22 +2103,26 @@ public sealed class BaseItemRepository
|
||||
if (filter.MinParentalRating != null)
|
||||
{
|
||||
var min = filter.MinParentalRating;
|
||||
minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
|
||||
if (min.SubScore != null)
|
||||
{
|
||||
minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
|
||||
}
|
||||
var minScore = min.Score;
|
||||
var minSubScore = min.SubScore ?? 0;
|
||||
|
||||
minParentalRatingFilter = e =>
|
||||
e.InheritedParentalRatingValue == null ||
|
||||
e.InheritedParentalRatingValue > minScore ||
|
||||
(e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
|
||||
}
|
||||
|
||||
Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
|
||||
if (filter.MaxParentalRating != null)
|
||||
{
|
||||
var max = filter.MaxParentalRating;
|
||||
maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null;
|
||||
if (max.SubScore != null)
|
||||
{
|
||||
maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
|
||||
}
|
||||
var maxScore = max.Score;
|
||||
var maxSubScore = max.SubScore ?? 0;
|
||||
|
||||
maxParentalRatingFilter = e =>
|
||||
e.InheritedParentalRatingValue == null ||
|
||||
e.InheritedParentalRatingValue < maxScore ||
|
||||
(e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
|
||||
}
|
||||
|
||||
if (filter.HasParentalRating ?? false)
|
||||
@@ -2313,17 +2366,23 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.HasImdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
|
||||
baseQuery = filter.HasImdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
if (filter.HasTmdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
|
||||
baseQuery = filter.HasTmdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
if (filter.HasTvdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
|
||||
baseQuery = filter.HasTvdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
var queryTopParentIds = filter.TopParentIds;
|
||||
@@ -2361,39 +2420,34 @@ 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)));
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
&& (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
|
||||
!context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.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)
|
||||
// For seasons and episodes, we also need to check the parent series' tags.
|
||||
if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
|
||||
{
|
||||
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))));
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.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}\""));
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && 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)));
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -82,11 +84,14 @@ public class ChapterRepository : IChapterRepository
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapters(Guid itemId)
|
||||
public async Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
context.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDelete();
|
||||
context.SaveChanges();
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
await dbContext.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#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 MediaBrowser.Controller.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -18,39 +21,50 @@ 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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
.ToArray();
|
||||
|
||||
var toAdd = people
|
||||
.Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
|
||||
.Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
|
||||
.Select(Map);
|
||||
context.Peoples.AddRange(toAdd);
|
||||
@@ -108,6 +109,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
|
||||
var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
|
||||
if (existingMap is null)
|
||||
|
||||
@@ -13,8 +13,7 @@ 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", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
|
||||
/// <summary>
|
||||
/// Tests the available storage capacity on the jellyfin paths with estimated minimum values.
|
||||
@@ -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, TwoGigabyte);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +74,7 @@ public static class StorageHelper
|
||||
var drive = new DriveInfo(path);
|
||||
if (threshold != -1 && drive.AvailableFreeSpace < threshold)
|
||||
{
|
||||
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}.");
|
||||
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Available: {HumanizeStorageSize(drive.AvailableFreeSpace)}, Required: {HumanizeStorageSize(threshold)}.");
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
|
||||
@@ -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.OpenWrite(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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses.Add(
|
||||
operation.Responses.TryAdd(
|
||||
"503",
|
||||
new OpenApiResponse
|
||||
{
|
||||
|
||||
@@ -66,15 +66,8 @@ public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("401"))
|
||||
{
|
||||
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("403"))
|
||||
{
|
||||
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
}
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var scheme = new OpenApiSecurityScheme
|
||||
{
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) .NET Foundation and Contributors
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Server.Infrastructure
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override FileMetadata GetFileInfo(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
var length = fileInfo.Length;
|
||||
// This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
{
|
||||
using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
length = RandomAccess.GetLength(fileHandle);
|
||||
}
|
||||
|
||||
return new FileMetadata
|
||||
{
|
||||
Exists = fileInfo.Exists,
|
||||
Length = length,
|
||||
LastModified = fileInfo.LastWriteTimeUtc
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (range is not null && rangeLength == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
|
||||
if (!IsSymLink(result.FileName))
|
||||
{
|
||||
await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
|
||||
if (range is not null)
|
||||
{
|
||||
await SendFileAsync(
|
||||
result.FileName,
|
||||
response,
|
||||
offset: range.From ?? 0L,
|
||||
count: rangeLength).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendFileAsync(
|
||||
result.FileName,
|
||||
response,
|
||||
offset: 0,
|
||||
count: null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fileInfo = GetFileInfo(filePath);
|
||||
if (offset < 0 || offset > fileInfo.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
|
||||
}
|
||||
|
||||
if (count.HasValue
|
||||
&& (count.Value < 0 || count.Value > fileInfo.Length - offset))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
|
||||
}
|
||||
|
||||
// Copied from SendFileFallback.SendFileAsync
|
||||
const int BufferSize = 1024 * 16;
|
||||
|
||||
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||
|
||||
var fileStream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
bufferSize: BufferSize,
|
||||
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
localCancel.ThrowIfCancellationRequested();
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
await StreamCopyOperation
|
||||
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException) when (useRequestAborted)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
<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">
|
||||
|
||||
47
Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
Normal file
47
Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up all Music artists that have been migrated in the 10.11 RC migrations.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-10-09T20:00:00", nameof(CleanMusicArtist))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
public class CleanMusicArtist : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly IStartupLogger<CleanMusicArtist> _startupLogger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanMusicArtist"/> class.
|
||||
/// </summary>
|
||||
/// <param name="startupLogger">The startup logger.</param>
|
||||
/// <param name="dbContextFactory">The Db context factory.</param>
|
||||
public CleanMusicArtist(IStartupLogger<CleanMusicArtist> startupLogger, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||
{
|
||||
_startupLogger = startupLogger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
var peoples = context.Peoples.Where(e => e.PersonType == nameof(PersonKind.Artist) || e.PersonType == nameof(PersonKind.AlbumArtist));
|
||||
_startupLogger.LogInformation("Delete {Number} Artist and Album Artist person types from db", await peoples.CountAsync(cancellationToken).ConfigureAwait(false));
|
||||
|
||||
await peoples
|
||||
.ExecuteDeleteAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,25 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
};
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
var activityLogPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(activityLogPath))
|
||||
{
|
||||
_logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={activityLogPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
|
||||
userDbConnection.Open();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -122,6 +122,16 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
{
|
||||
lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
@@ -135,14 +145,21 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
return Path.Join(keyframeCachePath, prefix, filename);
|
||||
}
|
||||
|
||||
private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
|
||||
private bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
|
||||
{
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
var bytes = File.ReadAllBytes(cachePath);
|
||||
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(cachePath);
|
||||
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
|
||||
|
||||
return cachedResult is not null;
|
||||
return cachedResult is not null;
|
||||
}
|
||||
catch (JsonException jsonException)
|
||||
{
|
||||
_logger.LogWarning(jsonException, "Failed to read {Path}", cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
cachedResult = null;
|
||||
|
||||
@@ -257,6 +257,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!baseItemIds.Contains(refItem.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.ItemId = refItem.Id;
|
||||
operation.JellyfinDbContext.UserData.Add(userData);
|
||||
}
|
||||
@@ -287,7 +292,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
|
||||
{
|
||||
operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
|
||||
var entity = GetMediaStream(dto);
|
||||
if (!baseItemIds.Contains(entity.ItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.MediaStreamInfos.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +321,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
|
||||
{
|
||||
operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto));
|
||||
var entity = GetMediaAttachment(dto);
|
||||
if (!baseItemIds.Contains(entity.ItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.AttachmentStreamInfos.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,8 +383,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
});
|
||||
}
|
||||
|
||||
baseItemIds.Clear();
|
||||
|
||||
foreach (var item in peopleCache)
|
||||
{
|
||||
operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
|
||||
@@ -396,6 +411,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
|
||||
{
|
||||
var chapter = GetChapter(dto);
|
||||
if (!baseItemIds.Contains(chapter.ItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.Chapters.Add(chapter);
|
||||
}
|
||||
}
|
||||
@@ -422,6 +442,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
|
||||
{
|
||||
var ancestorId = GetAncestorId(dto);
|
||||
if (!baseItemIds.Contains(ancestorId.ItemId) || !baseItemIds.Contains(ancestorId.ParentItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,28 @@ public class MigrateUserDb : IMigrationRoutine
|
||||
public void Perform()
|
||||
{
|
||||
var dataPath = _paths.DataPath;
|
||||
var userDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(userDbPath))
|
||||
{
|
||||
_logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
using (var connection = new SqliteConnection($"Filename={userDbPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _provider.CreateDbContext();
|
||||
|
||||
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
||||
|
||||
@@ -224,6 +224,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -263,6 +275,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
{
|
||||
date = File.GetLastWriteTimeUtc(path);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
@@ -184,6 +184,12 @@ namespace Jellyfin.Server
|
||||
.AddSingleton<IServiceCollection>(e))
|
||||
.Build();
|
||||
|
||||
/*
|
||||
* Initialize the transcode path marker so we avoid starting Jellyfin in a broken state.
|
||||
* This should really be a part of IApplicationPaths but this path is configured differently.
|
||||
*/
|
||||
_ = appHost.ConfigurationManager.GetTranscodePath();
|
||||
|
||||
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
|
||||
appHost.ServiceProvider = _jellyfinHost.Services;
|
||||
PrepareDatabaseProvider(appHost.ServiceProvider);
|
||||
|
||||
@@ -250,6 +250,7 @@ public sealed class SetupServer : IDisposable
|
||||
{ "isInReportingMode", _isUnhealthy },
|
||||
{ "retryValue", retryAfterValue },
|
||||
{ "logs", startupLogEntries },
|
||||
{ "networkManagerReady", networkManager is not null },
|
||||
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
|
||||
},
|
||||
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
|
||||
|
||||
@@ -213,7 +213,12 @@
|
||||
</ol>
|
||||
</div>
|
||||
{{#ELSE}}
|
||||
{{#IF networkManagerReady}}
|
||||
<p>Please visit this page from your local network to view detailed startup logs.</p>
|
||||
{{#ELSE}}
|
||||
<p>Initializing network settings. Please wait.</p>
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
</div>
|
||||
|
||||
@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.HealthChecks;
|
||||
using Jellyfin.Server.Implementations.Extensions;
|
||||
using Jellyfin.Server.Infrastructure;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.XbmcMetadata;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
|
||||
options.HttpsPort = _serverApplicationHost.HttpsPort;
|
||||
});
|
||||
|
||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
|
||||
services.AddJellyfinApiSwagger();
|
||||
|
||||
@@ -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.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</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; }
|
||||
|
||||
@@ -48,8 +48,10 @@ public interface IChapterManager
|
||||
Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the chapter images.
|
||||
/// Deletes the chapter data.
|
||||
/// </summary>
|
||||
/// <param name="video">Video to use.</param>
|
||||
void DeleteChapterImages(Video video);
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -337,6 +337,11 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
try
|
||||
{
|
||||
if (GetParents().Any(f => f.Id.Equals(Id)))
|
||||
{
|
||||
throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
|
||||
}
|
||||
|
||||
await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
@@ -452,6 +457,12 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
foreach (var item in itemsRemoved)
|
||||
{
|
||||
if (!item.CanDelete())
|
||||
{
|
||||
Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||
@@ -704,14 +715,21 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
else
|
||||
{
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
// Save pagination params before clearing them to prevent pagination from happening
|
||||
// before sorting. PostFilterAndSort will apply pagination after sorting.
|
||||
var limit = query.Limit;
|
||||
var startIndex = query.StartIndex;
|
||||
query.Limit = null;
|
||||
query.StartIndex = null; // override these here as they have already been applied
|
||||
query.StartIndex = null;
|
||||
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
// Restore pagination params so PostFilterAndSort can apply them after sorting
|
||||
query.Limit = limit;
|
||||
query.StartIndex = startIndex;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
result.TotalRecordCount = totalCount;
|
||||
return result;
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
if (this is not UserRootFolder
|
||||
@@ -969,25 +987,19 @@ namespace MediaBrowser.Controller.Entities
|
||||
else
|
||||
{
|
||||
// need to pass this param to the children.
|
||||
// Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
|
||||
var childQuery = new InternalItemsQuery
|
||||
{
|
||||
DisplayAlbumFolders = query.DisplayAlbumFolders,
|
||||
Limit = query.Limit,
|
||||
StartIndex = query.StartIndex,
|
||||
NameStartsWith = query.NameStartsWith,
|
||||
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
|
||||
NameLessThan = query.NameLessThan
|
||||
};
|
||||
|
||||
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
|
||||
|
||||
query.Limit = null;
|
||||
query.StartIndex = null;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
result.TotalRecordCount = totalItemCount;
|
||||
return result;
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
||||
@@ -1023,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(
|
||||
@@ -1036,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(
|
||||
@@ -1072,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)
|
||||
@@ -1346,6 +1405,14 @@ namespace MediaBrowser.Controller.Entities
|
||||
var realChildren = visibleChildren
|
||||
.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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -61,4 +62,82 @@ public static class FileSystemHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo;
|
||||
}
|
||||
|
||||
if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!targetInfo.Exists)
|
||||
{
|
||||
return targetInfo;
|
||||
}
|
||||
|
||||
var currentPath = targetInfo.FullName;
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
|
||||
while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2390,8 +2390,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|
||||
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
|
||||
{
|
||||
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
// If the video stream is in HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -6552,7 +6552,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
if (isD3d11Supported && isCodecAvailable)
|
||||
{
|
||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty)
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence;
|
||||
@@ -13,7 +15,9 @@ public interface IChapterRepository
|
||||
/// Deletes the chapters.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item.</param>
|
||||
void DeleteChapters(Guid itemId);
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the chapters.
|
||||
|
||||
@@ -18,10 +18,16 @@ namespace MediaBrowser.MediaEncoding.Configuration
|
||||
|
||||
public void Validate(object oldConfig, object newConfig)
|
||||
{
|
||||
var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
|
||||
var oldEncodingOptions = (EncodingOptions)oldConfig;
|
||||
var newEncodingOptions = (EncodingOptions)newConfig;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(oldEncodingOptions, nameof(oldConfig));
|
||||
ArgumentNullException.ThrowIfNull(newEncodingOptions, nameof(newConfig));
|
||||
|
||||
var newPath = newEncodingOptions.TranscodingTempPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
&& !string.Equals(oldEncodingOptions.TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
// Validate
|
||||
if (!Directory.Exists(newPath))
|
||||
@@ -33,6 +39,12 @@ namespace MediaBrowser.MediaEncoding.Configuration
|
||||
newPath));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newEncodingOptions.EncoderAppPath)
|
||||
&& !string.Equals(oldEncodingOptions.EncoderAppPath, newEncodingOptions.EncoderAppPath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Unable to update encoder app path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,7 +1122,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private void StartProcess(ProcessWrapper process)
|
||||
{
|
||||
process.Process.Start();
|
||||
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
|
||||
try
|
||||
{
|
||||
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
|
||||
}
|
||||
|
||||
lock (_runningProcessesLock)
|
||||
{
|
||||
|
||||
@@ -930,6 +930,15 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.Rotation = data.Rotation;
|
||||
}
|
||||
|
||||
// Parse video frame cropping metadata from side_data
|
||||
// TODO: save them and make HW filters to apply them in HWA pipelines
|
||||
else if (string.Equals(data.SideDataType, "Frame Cropping", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Streams containing artificially added frame cropping
|
||||
// metadata should not be marked as anamorphic.
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,12 +320,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 +343,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 +364,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 +402,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;
|
||||
|
||||
@@ -533,14 +533,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
private bool TryGetSanitizedAdditionalFields(Track track, string field, out string? value)
|
||||
{
|
||||
var hasField = track.AdditionalFields.TryGetValue(field, out value);
|
||||
var hasField = TryGetAdditionalFieldWithFallback(track, field, out value);
|
||||
value = GetSanitizedStringTag(value, track.Path);
|
||||
return hasField;
|
||||
}
|
||||
|
||||
private bool TryGetSanitizedUFIDFields(Track track, out string? owner, out string? identifier)
|
||||
{
|
||||
var hasField = track.AdditionalFields.TryGetValue("UFID", out string? value);
|
||||
var hasField = TryGetAdditionalFieldWithFallback(track, "UFID", out string? value);
|
||||
if (hasField && !string.IsNullOrEmpty(value))
|
||||
{
|
||||
string[] parts = value.Split('\0');
|
||||
@@ -556,5 +556,37 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
identifier = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build the explicit mka-style fallback key (e.g., ARTISTS -> track.artists, "MusicBrainz Artist Id" -> track.musicbrainz_artist_id)
|
||||
private static string GetMkaFallbackKey(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
var normalized = key.Trim().Replace(' ', '_').ToLowerInvariant();
|
||||
return "track." + normalized;
|
||||
}
|
||||
|
||||
// First try the normal key exactly; if missing, try the mka-style fallback key.
|
||||
private bool TryGetAdditionalFieldWithFallback(Track track, string key, out string? value)
|
||||
{
|
||||
// Prefer the normal key (as-is, case-sensitive)
|
||||
if (track.AdditionalFields.TryGetValue(key, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to mka-style: "track." + lower-case(original key)
|
||||
var fallbackKey = GetMkaFallbackKey(key);
|
||||
if (track.AdditionalFields.TryGetValue(fallbackKey, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
Name = person.Name,
|
||||
Type = person.Type,
|
||||
Role = person.Role.Trim()
|
||||
Role = person.Role?.Trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider
|
||||
var baseUrl = "https://www.theaudiodb.com/";
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
case MusicArtist:
|
||||
case Person:
|
||||
yield return baseUrl + $"artist/{externalId}";
|
||||
break;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
case MusicArtist:
|
||||
case Person:
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}";
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
}
|
||||
|
||||
var item = itemResult.Item;
|
||||
item.IndexNumber = episodeNumber;
|
||||
item.ParentIndexNumber = seasonNumber;
|
||||
|
||||
var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user