mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-16 17:42:24 +00:00
Compare commits
128 Commits
v10.9.1
...
feature/en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3738f87278 | ||
|
|
741a01db3b | ||
|
|
726026f0ae | ||
|
|
c7f87c0d69 | ||
|
|
4fa3c30df2 | ||
|
|
0d0a2b4d58 | ||
|
|
c1032967c2 | ||
|
|
4035f6aa21 | ||
|
|
dc2db22c3d | ||
|
|
2599babe31 | ||
|
|
8424ff5b61 | ||
|
|
b123f7ffcd | ||
|
|
9563e4f85e | ||
|
|
c4b7c91f3a | ||
|
|
1a94976752 | ||
|
|
407dc9272c | ||
|
|
5d4880c497 | ||
|
|
c0364fc766 | ||
|
|
76abff2fba | ||
|
|
a7d28045cb | ||
|
|
885df54cca | ||
|
|
93e66746f9 | ||
|
|
1f8dcea494 | ||
|
|
bbe2891ec5 | ||
|
|
de291fd7de | ||
|
|
01f88e4de5 | ||
|
|
b3bb031fca | ||
|
|
24d6532cf1 | ||
|
|
f3e39e87d7 | ||
|
|
4d5428ea90 | ||
|
|
c175371557 | ||
|
|
fc14c08bcc | ||
|
|
30b4ddeddf | ||
|
|
876ae44b8a | ||
|
|
39ae56db0a | ||
|
|
35bc6866d5 | ||
|
|
654dd2b704 | ||
|
|
2faa8c141f | ||
|
|
ac0064110b | ||
|
|
2af1ae5d8a | ||
|
|
833a1da355 | ||
|
|
e6dab2fa11 | ||
|
|
5c828df567 | ||
|
|
c7e0be3c3b | ||
|
|
e7145acd56 | ||
|
|
debd9eb8ce | ||
|
|
c3091b75a3 | ||
|
|
4430706915 | ||
|
|
487ebd3ca8 | ||
|
|
45400ac301 | ||
|
|
cbb99b6e6e | ||
|
|
063fabd344 | ||
|
|
cb9c848918 | ||
|
|
b8898e2338 | ||
|
|
41e92b34ad | ||
|
|
109112ba93 | ||
|
|
c975d50cdc | ||
|
|
575584b68f | ||
|
|
113d00f840 | ||
|
|
1567031046 | ||
|
|
98842b9357 | ||
|
|
8037382e8f | ||
|
|
70e85cb6c4 | ||
|
|
624ad9cb98 | ||
|
|
69ae006f37 | ||
|
|
d4f0b03982 | ||
|
|
d257c3c1bb | ||
|
|
03c23e15b3 | ||
|
|
00de8316ca | ||
|
|
e37e88f92f | ||
|
|
40820e3b41 | ||
|
|
c0f5fe9bd3 | ||
|
|
cbaafbc132 | ||
|
|
1f2c73b40a | ||
|
|
01946c6ef5 | ||
|
|
4ded042dde | ||
|
|
424ca49c26 | ||
|
|
a2eb4c5e60 | ||
|
|
0c159cd8b6 | ||
|
|
7336427ce6 | ||
|
|
8b938e2696 | ||
|
|
a7b2b92f2b | ||
|
|
5fe7d7f0bf | ||
|
|
e109e54949 | ||
|
|
8139179780 | ||
|
|
9a1a588857 | ||
|
|
b063dfd2e3 | ||
|
|
29a293f9e7 | ||
|
|
77c3ddc7ca | ||
|
|
9b978578ce | ||
|
|
4385430f05 | ||
|
|
4e2b30b193 | ||
|
|
7604c4b0f1 | ||
|
|
cb3691dd0d | ||
|
|
45fc7342f5 | ||
|
|
0cc5cc796d | ||
|
|
860c7da6e8 | ||
|
|
d318010c67 | ||
|
|
37b2d3aa2c | ||
|
|
279e91bb3d | ||
|
|
e619e19242 | ||
|
|
01c352d2e8 | ||
|
|
23d0537fb3 | ||
|
|
d622fc9281 | ||
|
|
435023a8f9 | ||
|
|
0173f7642b | ||
|
|
60fb3d5c06 | ||
|
|
69d4886697 | ||
|
|
610e56baaf | ||
|
|
5ac518b02a | ||
|
|
3564b00fc0 | ||
|
|
a118498f79 | ||
|
|
e5ecdcf8c9 | ||
|
|
1e0c7f05e6 | ||
|
|
bd255b3553 | ||
|
|
f568aed520 | ||
|
|
27ecf175d8 | ||
|
|
11a454c0fc | ||
|
|
8dd91ce9f8 | ||
|
|
92e5f946c1 | ||
|
|
fd250e4fe1 | ||
|
|
1ec130757d | ||
|
|
ce3e287892 | ||
|
|
13ed3329e0 | ||
|
|
25c23af865 | ||
|
|
4b7c41ee0f | ||
|
|
717b726329 | ||
|
|
04022f85af |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.4",
|
"version": "8.0.6",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -38,10 +38,11 @@ body:
|
|||||||
label: Jellyfin Version
|
label: Jellyfin Version
|
||||||
description: What version of Jellyfin are you running?
|
description: What version of Jellyfin are you running?
|
||||||
options:
|
options:
|
||||||
|
- 10.9.0
|
||||||
- 10.8.13
|
- 10.8.13
|
||||||
- 10.8.12
|
- 10.8.12 or older (please specify)
|
||||||
- 10.8.11 or older (please specify)
|
- Weekly unstable (please specify)
|
||||||
- Unstable (master branch)
|
- Master branch
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -20,18 +20,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/init@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/autobuild@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/analyze@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||||
|
|||||||
108
.github/workflows/ci-openapi.yml
vendored
108
.github/workflows/ci-openapi.yml
vendored
@@ -3,6 +3,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -14,7 +16,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -39,7 +41,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -99,11 +101,24 @@ jobs:
|
|||||||
- id: read-diff
|
- id: read-diff
|
||||||
name: Read openapi-diff output
|
name: Read openapi-diff output
|
||||||
run: |
|
run: |
|
||||||
|
# Read and fix markdown
|
||||||
body=$(cat openapi-changes.md)
|
body=$(cat openapi-changes.md)
|
||||||
body="${body//'%'/'%25'}"
|
# Write to workflow summary
|
||||||
body="${body//$'\n'/'%0A'}"
|
echo "$body" >> $GITHUB_STEP_SUMMARY
|
||||||
body="${body//$'\r'/'%0D'}"
|
# Set ApiChanged var
|
||||||
echo ::set-output name=body::$body
|
if [ "$body" != '' ]; then
|
||||||
|
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
# Add header/footer for diff comment
|
||||||
|
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
|
||||||
|
echo "<details>" >> openapi-changes-reply.md
|
||||||
|
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
|
||||||
|
echo "" >> openapi-changes-reply.md
|
||||||
|
echo "$body" >> openapi-changes-reply.md
|
||||||
|
echo "" >> openapi-changes-reply.md
|
||||||
|
echo "</details>" >> openapi-changes-reply.md
|
||||||
- name: Find difference comment
|
- name: Find difference comment
|
||||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||||
id: find-comment
|
id: find-comment
|
||||||
@@ -113,22 +128,15 @@ jobs:
|
|||||||
body-includes: openapi-diff-workflow-comment
|
body-includes: openapi-diff-workflow-comment
|
||||||
- name: Reply or edit difference comment (changed)
|
- 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@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
body: |
|
body-path: openapi-changes-reply.md
|
||||||
<!--openapi-diff-workflow-comment-->
|
|
||||||
<details>
|
|
||||||
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
|
||||||
|
|
||||||
${{ steps.read-diff.outputs.body }}
|
|
||||||
|
|
||||||
</details>
|
|
||||||
- name: Edit difference comment (unchanged)
|
- name: Edit difference comment (unchanged)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||||
@@ -138,11 +146,9 @@ jobs:
|
|||||||
|
|
||||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
||||||
|
|
||||||
publish:
|
publish-unstable:
|
||||||
name: OpenAPI - Publish Unstable Spec
|
name: OpenAPI - Publish Unstable Spec
|
||||||
if: |
|
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
github.event_name != 'pull_request_target' &&
|
|
||||||
contains(github.repository_owner, 'jellyfin')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- openapi-head
|
- openapi-head
|
||||||
@@ -201,3 +207,65 @@ jobs:
|
|||||||
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||||
fi
|
fi
|
||||||
) 200>/run/workflows/openapi-unstable.lock
|
) 200>/run/workflows/openapi-unstable.lock
|
||||||
|
|
||||||
|
publish-stable:
|
||||||
|
name: OpenAPI - Publish Stable Spec
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- openapi-head
|
||||||
|
steps:
|
||||||
|
- name: Set version number
|
||||||
|
id: version
|
||||||
|
run: |-
|
||||||
|
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
- name: Download openapi-head
|
||||||
|
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
path: openapi-head
|
||||||
|
- name: Upload openapi.json (stable) to repository server
|
||||||
|
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
source: openapi-head/openapi.json
|
||||||
|
strip_components: 1
|
||||||
|
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||||
|
- name: Move openapi.json (stable) into place
|
||||||
|
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
debug: false
|
||||||
|
script_stop: false
|
||||||
|
script: |
|
||||||
|
if ! test -d /run/workflows; then
|
||||||
|
sudo mkdir -p /run/workflows
|
||||||
|
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
flock -x -w 300 200 || exit 1
|
||||||
|
TGT_DIR="/srv/repository/main/openapi"
|
||||||
|
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
||||||
|
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
||||||
|
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
|
||||||
|
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Move new spec into place
|
||||||
|
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
||||||
|
# Delete previous jellyfin-openapi-stable_previous.json
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
|
||||||
|
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
# Create new jellyfin-openapi-stable.json symlink
|
||||||
|
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
|
||||||
|
# Check that the previous openapi stable spec link is correct
|
||||||
|
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
fi
|
||||||
|
) 200>/run/workflows/openapi-stable.lock
|
||||||
|
|||||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: "${{ matrix.os }}"
|
runs-on: "${{ matrix.os }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
with:
|
with:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
--verbosity minimal
|
--verbosity minimal
|
||||||
|
|
||||||
- name: Merge code coverage results
|
- name: Merge code coverage results
|
||||||
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5
|
uses: danielpalme/ReportGenerator-GitHub-Action@fa728091745cdd279fddda1e0e80fb29265d0977 # 5.3.5
|
||||||
with:
|
with:
|
||||||
reports: "**/coverage.cobertura.xml"
|
reports: "**/coverage.cobertura.xml"
|
||||||
targetdir: "merged/"
|
targetdir: "merged/"
|
||||||
|
|||||||
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
reactions: eyes
|
reactions: eyes
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -128,7 +128,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
- name: install python
|
- name: install python
|
||||||
|
|||||||
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
- name: install python
|
- name: install python
|
||||||
|
|||||||
2
.github/workflows/pull-request-conflict.yml
vendored
2
.github/workflows/pull-request-conflict.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
|
uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
|
|||||||
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
|
yq-version: v4.9.8
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@
|
|||||||
- [btopherjohnson](https://github.com/btopherjohnson)
|
- [btopherjohnson](https://github.com/btopherjohnson)
|
||||||
- [GeorgeH005](https://github.com/GeorgeH005)
|
- [GeorgeH005](https://github.com/GeorgeH005)
|
||||||
- [Vedant](https://github.com/viktory36/)
|
- [Vedant](https://github.com/viktory36/)
|
||||||
|
- [NotSaifA](https://github.com/NotSaifA)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
@@ -255,3 +256,4 @@
|
|||||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||||
- [Robert Lützner](https://github.com/rluetzner)
|
- [Robert Lützner](https://github.com/rluetzner)
|
||||||
|
- [Nathan McCrina](https://github.com/nfmccrina)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />
|
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
|
||||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
@@ -25,15 +25,14 @@
|
|||||||
<PackageVersion Include="libse" Version="4.0.5" />
|
<PackageVersion Include="libse" Version="4.0.5" />
|
||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
|
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
@@ -42,14 +41,14 @@
|
|||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.9.1</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
44
Emby.Naming/TV/TvParserHelpers.cs
Normal file
44
Emby.Naming/TV/TvParserHelpers.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
|
namespace Emby.Naming.TV;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for TV metadata parsing.
|
||||||
|
/// </summary>
|
||||||
|
public static class TvParserHelpers
|
||||||
|
{
|
||||||
|
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
|
||||||
|
private static readonly string[] _endedState = ["Cancelled", "Canceled"];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a string into <see cref="SeriesStatus"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">The status string.</param>
|
||||||
|
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
|
||||||
|
/// <returns>Returns true if parsing was successful.</returns>
|
||||||
|
public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue)
|
||||||
|
{
|
||||||
|
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
|
||||||
|
{
|
||||||
|
enumValue = seriesStatus;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_continuingState.Contains(status, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
enumValue = SeriesStatus.Continuing;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_endedState.Contains(status, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
enumValue = SeriesStatus.Ended;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
enumValue = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ namespace Emby.Server.Implementations
|
|||||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||||
{ BindToUnixSocketKey, bool.FalseString },
|
{ BindToUnixSocketKey, bool.FalseString },
|
||||||
{ SqliteCacheSizeKey, "20000" }
|
{ SqliteCacheSizeKey, "20000" },
|
||||||
|
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1298,16 +1298,15 @@ namespace Emby.Server.Implementations.Data
|
|||||||
&& type != typeof(Book)
|
&& type != typeof(Book)
|
||||||
&& type != typeof(LiveTvProgram)
|
&& type != typeof(LiveTvProgram)
|
||||||
&& type != typeof(AudioBook)
|
&& type != typeof(AudioBook)
|
||||||
&& type != typeof(Audio)
|
|
||||||
&& type != typeof(MusicAlbum);
|
&& type != typeof(MusicAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
|
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
|
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
|
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
|
||||||
{
|
{
|
||||||
var typeString = reader.GetString(0);
|
var typeString = reader.GetString(0);
|
||||||
|
|
||||||
@@ -1320,7 +1319,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
BaseItem item = null;
|
BaseItem item = null;
|
||||||
|
|
||||||
if (TypeRequiresDeserialization(type))
|
if (TypeRequiresDeserialization(type) && !skipDeserialization)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -2323,7 +2322,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
columns.Add(builder.ToString());
|
columns.Add(builder.ToString());
|
||||||
|
|
||||||
query.ExcludeItemIds = [..query.ExcludeItemIds, item.Id, ..item.ExtraIds];
|
query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds];
|
||||||
query.ExcludeProviderIds = item.ProviderIds;
|
query.ExcludeProviderIds = item.ProviderIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2562,7 +2561,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
{
|
{
|
||||||
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
|
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
|
||||||
if (item is not null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
items.Add(item);
|
items.Add(item);
|
||||||
@@ -2774,7 +2773,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
{
|
{
|
||||||
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
|
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
|
||||||
if (item is not null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
list.Add(item);
|
list.Add(item);
|
||||||
@@ -2831,7 +2830,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
|
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
|
||||||
}
|
}
|
||||||
|
|
||||||
orderBy = query.OrderBy = [..prepend, ..orderBy];
|
orderBy = query.OrderBy = [.. prepend, .. orderBy];
|
||||||
}
|
}
|
||||||
else if (orderBy.Count == 0)
|
else if (orderBy.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -5021,7 +5020,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
{
|
{
|
||||||
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
|
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
|
||||||
if (item is not null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
var countStartColumn = columns.Count - 1;
|
var countStartColumn = columns.Count - 1;
|
||||||
@@ -5144,7 +5143,7 @@ AND Type = @InternalPersonType)");
|
|||||||
list.AddRange(inheritedTags.Select(i => (6, i)));
|
list.AddRange(inheritedTags.Select(i => (6, i)));
|
||||||
|
|
||||||
// Remove all invalid values.
|
// Remove all invalid values.
|
||||||
list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
|
list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -5202,12 +5201,6 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
var itemValue = currentValueInfo.Value;
|
var itemValue = currentValueInfo.Value;
|
||||||
|
|
||||||
// Don't save if invalid
|
|
||||||
if (string.IsNullOrWhiteSpace(itemValue))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
|
statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
|
||||||
statement.TryBind("@Value" + index, itemValue);
|
statement.TryBind("@Value" + index, itemValue);
|
||||||
statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
|
statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
|
||||||
@@ -5228,19 +5221,20 @@ AND Type = @InternalPersonType)");
|
|||||||
throw new ArgumentNullException(nameof(itemId));
|
throw new ArgumentNullException(nameof(itemId));
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(people);
|
|
||||||
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using var connection = GetConnection();
|
using var connection = GetConnection();
|
||||||
using var transaction = connection.BeginTransaction();
|
using var transaction = connection.BeginTransaction();
|
||||||
// First delete chapters
|
// Delete all existing people first
|
||||||
using var command = connection.CreateCommand();
|
using var command = connection.CreateCommand();
|
||||||
command.CommandText = "delete from People where ItemId=@ItemId";
|
command.CommandText = "delete from People where ItemId=@ItemId";
|
||||||
command.TryBind("@ItemId", itemId);
|
command.TryBind("@ItemId", itemId);
|
||||||
command.ExecuteNonQuery();
|
command.ExecuteNonQuery();
|
||||||
|
|
||||||
InsertPeople(itemId, people, connection);
|
if (people is not null)
|
||||||
|
{
|
||||||
|
InsertPeople(itemId, people, connection);
|
||||||
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Images
|
namespace Emby.Server.Implementations.Images
|
||||||
{
|
{
|
||||||
@@ -33,12 +32,12 @@ namespace Emby.Server.Implementations.Images
|
|||||||
Parent = item,
|
Parent = item,
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
DtoOptions = new DtoOptions(true),
|
DtoOptions = new DtoOptions(true),
|
||||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
ImageTypes = [ImageType.Primary],
|
||||||
OrderBy = new (ItemSortBy, SortOrder)[]
|
OrderBy =
|
||||||
{
|
[
|
||||||
(ItemSortBy.IsFolder, SortOrder.Ascending),
|
(ItemSortBy.IsFolder, SortOrder.Ascending),
|
||||||
(ItemSortBy.SortName, SortOrder.Ascending)
|
(ItemSortBy.SortName, SortOrder.Ascending)
|
||||||
},
|
],
|
||||||
Limit = 1
|
Limit = 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -15,5 +18,13 @@ namespace Emby.Server.Implementations.Images
|
|||||||
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
|
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
|
||||||
|
{
|
||||||
|
var items = base.GetItemsWithImages(item);
|
||||||
|
|
||||||
|
// Ignore any folders because they can have generated collages
|
||||||
|
return items.Where(i => i is not Folder).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2812,8 +2812,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.UpdatePeople(item.Id, people);
|
_itemRepository.UpdatePeople(item.Id, people);
|
||||||
|
if (people is not null)
|
||||||
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
|
{
|
||||||
|
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure)
|
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Naming.Audio;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
@@ -85,6 +86,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
}
|
}
|
||||||
|
|
||||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
|
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
|
||||||
|
var albumParser = new AlbumParser(_namingOptions);
|
||||||
|
|
||||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||||
|
|
||||||
@@ -100,6 +102,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the folder is a multi-disc folder, then it is not an artist folder
|
||||||
|
if (albumParser.IsMultiPart(fileSystemInfo.FullName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If we contain a music album assume we are an artist folder
|
// If we contain a music album assume we are an artist folder
|
||||||
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
|
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
{
|
{
|
||||||
IndexNumber = seasonParserResult.SeasonNumber,
|
IndexNumber = seasonParserResult.SeasonNumber,
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeriesName = series.Name
|
SeriesName = series.Name,
|
||||||
|
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
||||||
@@ -78,27 +79,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (season.IndexNumber.HasValue)
|
if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
|
||||||
{
|
{
|
||||||
var seasonNumber = season.IndexNumber.Value;
|
var seasonNumber = season.IndexNumber.Value;
|
||||||
if (string.IsNullOrEmpty(season.Name))
|
season.Name = seasonNumber == 0 ?
|
||||||
{
|
args.LibraryOptions.SeasonZeroDisplayName :
|
||||||
var seasonNames = series.SeasonNames;
|
string.Format(
|
||||||
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
|
CultureInfo.InvariantCulture,
|
||||||
{
|
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||||
season.Name = seasonName;
|
seasonNumber,
|
||||||
}
|
args.LibraryOptions.PreferredMetadataLanguage);
|
||||||
else
|
|
||||||
{
|
|
||||||
season.Name = seasonNumber == 0 ?
|
|
||||||
args.LibraryOptions.SeasonZeroDisplayName :
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
|
||||||
seasonNumber,
|
|
||||||
args.LibraryOptions.PreferredMetadataLanguage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return season;
|
return season;
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"Albums": "аальбомқәа"
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,5 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
|
||||||
|
"TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
|
||||||
|
"TaskAudioNormalization": "Нармалізацыя гуку"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||||
"HeaderLiveTV": "Živý přenos",
|
"HeaderLiveTV": "TV vysílání",
|
||||||
"HeaderNextUp": "Další díly",
|
"HeaderNextUp": "Další díly",
|
||||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||||
"HomeVideos": "Domácí videa",
|
"HomeVideos": "Domácí videa",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"Genres": "Genrer",
|
"Genres": "Genrer",
|
||||||
"HeaderAlbumArtists": "Albumkunstnere",
|
"HeaderAlbumArtists": "Albumkunstnere",
|
||||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
"HeaderFavoriteAlbums": "Favoritalbum",
|
||||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||||
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
||||||
"HeaderFavoriteShows": "Yndlingsserier",
|
"HeaderFavoriteShows": "Yndlingsserier",
|
||||||
@@ -87,21 +87,21 @@
|
|||||||
"UserOnlineFromDevice": "{0} er online fra {1}",
|
"UserOnlineFromDevice": "{0} er online fra {1}",
|
||||||
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
||||||
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
|
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1} på {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||||
"ValueSpecialEpisodeName": "Special - {0}",
|
"ValueSpecialEpisodeName": "Special - {0}",
|
||||||
"VersionNumber": "Version {0}",
|
"VersionNumber": "Version {0}",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
||||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
|
||||||
"TaskUpdatePlugins": "Opdater Plugins",
|
"TaskUpdatePlugins": "Opdater Plugins",
|
||||||
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
||||||
"TaskCleanLogs": "Ryd Log-mappe",
|
"TaskCleanLogs": "Ryd Log-mappe",
|
||||||
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
||||||
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
||||||
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
||||||
"TaskCleanCache": "Ryd Cache-mappe",
|
"TaskCleanCache": "Ryd cache-mappe",
|
||||||
"TasksChannelsCategory": "Internetkanaler",
|
"TasksChannelsCategory": "Internetkanaler",
|
||||||
"TasksApplicationCategory": "Applikation",
|
"TasksApplicationCategory": "Applikation",
|
||||||
"TasksLibraryCategory": "Bibliotek",
|
"TasksLibraryCategory": "Bibliotek",
|
||||||
@@ -128,5 +128,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
|
||||||
|
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
|
||||||
|
"TaskAudioNormalization": "Audio-normalisering"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,5 +126,9 @@
|
|||||||
"External": "Εξωτερικό",
|
"External": "Εξωτερικό",
|
||||||
"HearingImpaired": "Με προβλήματα ακοής",
|
"HearingImpaired": "Με προβλήματα ακοής",
|
||||||
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
|
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||||
|
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||||
|
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Colecciones",
|
"Collections": "Colecciones",
|
||||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||||
"DeviceOnlineWithName": "{0} está conectado",
|
"DeviceOnlineWithName": "{0} está conectado",
|
||||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
|
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
@@ -124,5 +124,11 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
|
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
|
||||||
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
|
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"HearingImpaired": "Discapacidad Auditiva"
|
"HearingImpaired": "Discapacidad Auditiva",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
|
||||||
|
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
||||||
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Colecciones",
|
"Collections": "Colecciones",
|
||||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||||
"DeviceOnlineWithName": "{0} está conectado",
|
"DeviceOnlineWithName": "{0} está conectado",
|
||||||
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
|
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
|
|||||||
@@ -12,14 +12,118 @@
|
|||||||
"Application": "Aplicación",
|
"Application": "Aplicación",
|
||||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||||
"HeaderContinueWatching": "Continuar Viendo",
|
"HeaderContinueWatching": "Continuar Viendo",
|
||||||
"HeaderAlbumArtists": "Artistas del Álbum",
|
"HeaderAlbumArtists": "Artistas del álbum",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
|
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
|
||||||
"HeaderFavoriteSongs": "Canciones Favoritas",
|
"HeaderFavoriteSongs": "Canciones Favoritas",
|
||||||
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
||||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"Default": "Predeterminado"
|
"Default": "Predeterminado",
|
||||||
|
"Movies": "Películas",
|
||||||
|
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
|
||||||
|
"MixedContent": "Contenido mixto",
|
||||||
|
"Music": "Música",
|
||||||
|
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
|
||||||
|
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
|
||||||
|
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
|
||||||
|
"Sync": "Sincronizar",
|
||||||
|
"Shows": "Series",
|
||||||
|
"UserDownloadingItemWithValues": "{0} está descargando {1}",
|
||||||
|
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
|
||||||
|
"UserOnlineFromDevice": "{0} está en línea desde {1}",
|
||||||
|
"TasksChannelsCategory": "Canales de Internet",
|
||||||
|
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
|
||||||
|
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
|
||||||
|
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||||
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
|
||||||
|
"TvShows": "Series de TV",
|
||||||
|
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
||||||
|
"TaskRefreshChannels": "Actualizar canales",
|
||||||
|
"Photos": "Fotos",
|
||||||
|
"HeaderFavoriteShows": "Programas favoritos",
|
||||||
|
"TaskCleanActivityLog": "Limpiar registro de actividades",
|
||||||
|
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
|
||||||
|
"System": "Sistema",
|
||||||
|
"User": "Usuario",
|
||||||
|
"Forced": "Forzado",
|
||||||
|
"PluginInstalledWithName": "{0} ha sido instalado",
|
||||||
|
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||||
|
"TaskUpdatePlugins": "Actualizar Plugins",
|
||||||
|
"Latest": "Recientes",
|
||||||
|
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
|
||||||
|
"Songs": "Canciones",
|
||||||
|
"NotificationOptionPluginError": "Falla de plugin",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} iniciado",
|
||||||
|
"TasksApplicationCategory": "Aplicación",
|
||||||
|
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
|
||||||
|
"TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
|
||||||
|
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para plugins que están configurados para actualizarse automáticamente.",
|
||||||
|
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
|
||||||
|
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||||
|
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
|
||||||
|
"TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
|
||||||
|
"NotificationOptionPluginUpdateInstalled": "Actualización de plugin instalada",
|
||||||
|
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
|
||||||
|
"TasksLibraryCategory": "Biblioteca",
|
||||||
|
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||||
|
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
|
||||||
|
"VersionNumber": "Versión {0}",
|
||||||
|
"HeaderNextUp": "A continuación",
|
||||||
|
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
|
||||||
|
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||||
|
"NameSeasonNumber": "Temporada {0}",
|
||||||
|
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
|
||||||
|
"Plugin": "Plugin",
|
||||||
|
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
|
||||||
|
"NotificationOptionTaskFailed": "Falló la tarea programada",
|
||||||
|
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
|
||||||
|
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
|
||||||
|
"TaskRefreshLibrary": "Escanear biblioteca de medios",
|
||||||
|
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
|
||||||
|
"TasksMaintenanceCategory": "Mantenimiento",
|
||||||
|
"ProviderValue": "Proveedor: {0}",
|
||||||
|
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||||
|
"PluginUninstalledWithName": "{0} ha sido desinstalado",
|
||||||
|
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} falló",
|
||||||
|
"TaskCleanLogs": "Limpiar directorio de registros",
|
||||||
|
"NameInstallFailed": "Falló la instalación de {0}",
|
||||||
|
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
|
||||||
|
"TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
|
||||||
|
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
|
||||||
|
"Playlists": "Listas de reproducción",
|
||||||
|
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
|
||||||
|
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
|
||||||
|
"TaskRefreshPeople": "Actualizar personas",
|
||||||
|
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
|
||||||
|
"HeaderLiveTV": "TV en vivo",
|
||||||
|
"NameSeasonUnknown": "Temporada desconocida",
|
||||||
|
"NotificationOptionInstallationFailed": "Fallo de instalación",
|
||||||
|
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||||
|
"TaskCleanCache": "Limpiar directorio caché",
|
||||||
|
"TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
|
||||||
|
"Inherit": "Heredar",
|
||||||
|
"HeaderRecordingGroups": "Grupos de grabación",
|
||||||
|
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||||
|
"TaskOptimizeDatabase": "Optimizar base de datos",
|
||||||
|
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
||||||
|
"HearingImpaired": "Discapacidad auditiva",
|
||||||
|
"HomeVideos": "Videos caseros",
|
||||||
|
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
||||||
|
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||||
|
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||||
|
"MusicVideos": "Videos musicales",
|
||||||
|
"NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
|
||||||
|
"PluginUpdatedWithName": "{0} ha sido actualizado",
|
||||||
|
"Undefined": "Sin definir",
|
||||||
|
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
|
||||||
|
"TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
|
||||||
|
"TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,5 +125,7 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
||||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
|
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
|
||||||
|
"TaskAudioNormalization": "Heli Normaliseerimine",
|
||||||
|
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,5 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat"
|
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
|
||||||
|
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
||||||
|
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||||
"DeviceOnlineWithName": "{0} est connecté",
|
"DeviceOnlineWithName": "{0} est connecté",
|
||||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
|
||||||
"Favorites": "Favoris",
|
"Favorites": "Favoris",
|
||||||
"Folders": "Dossiers",
|
"Folders": "Dossiers",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"MixedContent": "Contenu mixte",
|
"MixedContent": "Contenu mixte",
|
||||||
"Movies": "Films",
|
"Movies": "Films",
|
||||||
"Music": "Musique",
|
"Music": "Musique",
|
||||||
"MusicVideos": "Vidéos musicales",
|
"MusicVideos": "Vidéoclips",
|
||||||
"NameInstallFailed": "échec d'installation de {0}",
|
"NameInstallFailed": "échec d'installation de {0}",
|
||||||
"NameSeasonNumber": "Saison {0}",
|
"NameSeasonNumber": "Saison {0}",
|
||||||
"NameSeasonUnknown": "Saison Inconnue",
|
"NameSeasonUnknown": "Saison Inconnue",
|
||||||
@@ -128,5 +128,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprimer les liens inexistants des collections et des listes de lecture"
|
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
||||||
|
"TaskAudioNormalization": "Normalisation audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,5 +126,9 @@
|
|||||||
"External": "חיצוני",
|
"External": "חיצוני",
|
||||||
"HearingImpaired": "לקוי שמיעה",
|
"HearingImpaired": "לקוי שמיעה",
|
||||||
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
||||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
|
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
|
||||||
|
"TaskAudioNormalization": "נרמול שמע",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
||||||
|
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
"Movies": "Film",
|
"Movies": "Film",
|
||||||
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
||||||
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
|
"FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
|
||||||
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
||||||
"DeviceOfflineWithName": "{0} telah terputus",
|
"DeviceOfflineWithName": "{0} telah terputus",
|
||||||
"DeviceOnlineWithName": "{0} telah terhubung",
|
"DeviceOnlineWithName": "{0} telah terhubung",
|
||||||
@@ -125,5 +125,9 @@
|
|||||||
"External": "Luar",
|
"External": "Luar",
|
||||||
"HearingImpaired": "Gangguan Pendengaran",
|
"HearingImpaired": "Gangguan Pendengaran",
|
||||||
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
|
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
|
||||||
|
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
||||||
|
"TaskAudioNormalization": "Normalisasi Audio",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
||||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||||
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
|
||||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Collecties",
|
"Collections": "Collecties",
|
||||||
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
||||||
"DeviceOnlineWithName": "{0} is verbonden",
|
"DeviceOnlineWithName": "{0} is verbonden",
|
||||||
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
|
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
|
||||||
"Favorites": "Favorieten",
|
"Favorites": "Favorieten",
|
||||||
"Folders": "Mappen",
|
"Folders": "Mappen",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
||||||
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
||||||
"External": "Extern",
|
"External": "Extern",
|
||||||
"HearingImpaired": "Slechthorend",
|
"HearingImpaired": "Slechthorenden",
|
||||||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
||||||
|
|||||||
@@ -118,5 +118,6 @@
|
|||||||
"Undefined": "Udefinert",
|
"Undefined": "Udefinert",
|
||||||
"Forced": "Tvungen",
|
"Forced": "Tvungen",
|
||||||
"Default": "Standard",
|
"Default": "Standard",
|
||||||
"External": "Ekstern"
|
"External": "Ekstern",
|
||||||
|
"HearingImpaired": "Nedsett høyrsel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Kolekcje",
|
"Collections": "Kolekcje",
|
||||||
"DeviceOfflineWithName": "{0} został rozłączony",
|
"DeviceOfflineWithName": "{0} został rozłączony",
|
||||||
"DeviceOnlineWithName": "{0} połączył się",
|
"DeviceOnlineWithName": "{0} połączył się",
|
||||||
"FailedLoginAttemptWithUserName": "Próba logowania przez {0} zakończona niepowodzeniem",
|
"FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
|
||||||
"Favorites": "Ulubione",
|
"Favorites": "Ulubione",
|
||||||
"Folders": "Foldery",
|
"Folders": "Foldery",
|
||||||
"Genres": "Gatunki",
|
"Genres": "Gatunki",
|
||||||
@@ -98,8 +98,8 @@
|
|||||||
"TaskRefreshChannels": "Odśwież kanały",
|
"TaskRefreshChannels": "Odśwież kanały",
|
||||||
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
||||||
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
||||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
|
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje wtyczek, które są skonfigurowane do automatycznej aktualizacji.",
|
||||||
"TaskUpdatePlugins": "Aktualizuj pluginy",
|
"TaskUpdatePlugins": "Aktualizuj wtyczki",
|
||||||
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
||||||
"TaskRefreshPeople": "Odśwież obsadę",
|
"TaskRefreshPeople": "Odśwież obsadę",
|
||||||
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
||||||
|
|||||||
@@ -130,5 +130,5 @@
|
|||||||
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
||||||
"TaskAudioNormalization": "Normalização de áudio",
|
"TaskAudioNormalization": "Normalização de áudio",
|
||||||
"TaskAudioNormalizationDescription": "Verifica arquivos em busca de dados de normalização de áudio."
|
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Коллекции",
|
"Collections": "Коллекции",
|
||||||
"DeviceOfflineWithName": "{0} - отключено",
|
"DeviceOfflineWithName": "{0} - отключено",
|
||||||
"DeviceOnlineWithName": "{0} - подключено",
|
"DeviceOnlineWithName": "{0} - подключено",
|
||||||
"FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
|
"FailedLoginAttemptWithUserName": "Неудачная попытка входа с {0}",
|
||||||
"Favorites": "Избранное",
|
"Favorites": "Избранное",
|
||||||
"Folders": "Папки",
|
"Folders": "Папки",
|
||||||
"Genres": "Жанры",
|
"Genres": "Жанры",
|
||||||
@@ -128,5 +128,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
|
||||||
|
"TaskAudioNormalization": "Нормализация звука",
|
||||||
|
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,5 +127,8 @@
|
|||||||
"HearingImpaired": "Hörselskadad",
|
"HearingImpaired": "Hörselskadad",
|
||||||
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Rensa samlingar och spellistor"
|
"TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
|
||||||
|
"TaskAudioNormalization": "Ljudnormalisering",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
|
||||||
|
"TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,5 +125,9 @@
|
|||||||
"External": "வெளி",
|
"External": "வெளி",
|
||||||
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
||||||
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
||||||
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
|
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
|
||||||
|
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
|
||||||
|
"TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Koleksiyonlar",
|
"Collections": "Koleksiyonlar",
|
||||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||||
"DeviceOnlineWithName": "{0} bağlı",
|
"DeviceOnlineWithName": "{0} bağlı",
|
||||||
"FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
|
"FailedLoginAttemptWithUserName": "{0} kullanıcısının başarısız oturum açma girişimi",
|
||||||
"Favorites": "Favoriler",
|
"Favorites": "Favoriler",
|
||||||
"Folders": "Klasörler",
|
"Folders": "Klasörler",
|
||||||
"Genres": "Türler",
|
"Genres": "Türler",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
||||||
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
||||||
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
||||||
"FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
|
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
|
||||||
"DeviceOnlineWithName": "{0} đã kết nối",
|
"DeviceOnlineWithName": "{0} đã kết nối",
|
||||||
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
||||||
"ChapterNameValue": "Phân Cảnh {0}",
|
"ChapterNameValue": "Phân Cảnh {0}",
|
||||||
@@ -127,5 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
|
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
|
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.",
|
||||||
|
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
|
||||||
|
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "合集",
|
"Collections": "合集",
|
||||||
"DeviceOfflineWithName": "{0} 已断开",
|
"DeviceOfflineWithName": "{0} 已断开",
|
||||||
"DeviceOnlineWithName": "{0} 已连接",
|
"DeviceOnlineWithName": "{0} 已连接",
|
||||||
"FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
|
"FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败",
|
||||||
"Favorites": "我的最爱",
|
"Favorites": "我的最爱",
|
||||||
"Folders": "文件夹",
|
"Folders": "文件夹",
|
||||||
"Genres": "类型",
|
"Genres": "类型",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
@@ -69,7 +70,7 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => "AudioNormalization";
|
public string Key => "AudioNormalization";
|
||||||
|
|
||||||
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
[GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
|
||||||
private static partial Regex LUFSRegex();
|
private static partial Regex LUFSRegex();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -179,16 +180,17 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var reader = process.StandardError;
|
using var reader = process.StandardError;
|
||||||
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
MatchCollection split = LUFSRegex().Matches(output);
|
|
||||||
|
|
||||||
if (split.Count != 0)
|
|
||||||
{
|
{
|
||||||
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
Match match = LUFSRegex().Match(line);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
|
_logger.LogError("Failed to find LUFS value in output");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1202,7 +1202,8 @@ namespace Emby.Server.Implementations.Session
|
|||||||
new DtoOptions(false)
|
new DtoOptions(false)
|
||||||
{
|
{
|
||||||
EnableImages = false
|
EnableImages = false
|
||||||
})
|
},
|
||||||
|
user.DisplayMissingEpisodes)
|
||||||
.Where(i => !i.IsVirtualItem)
|
.Where(i => !i.IsVirtualItem)
|
||||||
.SkipWhile(i => !i.Id.Equals(episode.Id))
|
.SkipWhile(i => !i.Id.Equals(episode.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Api.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
@@ -24,20 +25,31 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
|
||||||
{
|
{
|
||||||
|
// Succeed if the startup wizard / first time setup is not complete
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
|
|
||||||
|
// Succeed if user is admin
|
||||||
|
else if (context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
|
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fail if admin is required and user is not admin
|
||||||
|
else if (requirement.RequireAdmin)
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Succeed if admin is not required and user is not guest
|
||||||
|
else if (context.User.IsInRole(UserRoles.User))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,17 +290,35 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
foreach (var season in rseries.Children.OfType<Season>())
|
foreach (var season in rseries.Children.OfType<Season>())
|
||||||
{
|
{
|
||||||
season.OfficialRating = request.OfficialRating;
|
if (!season.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
|
{
|
||||||
|
season.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
season.CustomRating = request.CustomRating;
|
season.CustomRating = request.CustomRating;
|
||||||
season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
|
||||||
|
if (!season.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
|
season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
season.OnMetadataChanged();
|
season.OnMetadataChanged();
|
||||||
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var ep in season.Children.OfType<Episode>())
|
foreach (var ep in season.Children.OfType<Episode>())
|
||||||
{
|
{
|
||||||
ep.OfficialRating = request.OfficialRating;
|
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
|
{
|
||||||
|
ep.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
ep.CustomRating = request.CustomRating;
|
ep.CustomRating = request.CustomRating;
|
||||||
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
|
||||||
|
if (!ep.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
|
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
ep.OnMetadataChanged();
|
ep.OnMetadataChanged();
|
||||||
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -310,9 +328,18 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
foreach (var ep in season.Children.OfType<Episode>())
|
foreach (var ep in season.Children.OfType<Episode>())
|
||||||
{
|
{
|
||||||
ep.OfficialRating = request.OfficialRating;
|
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
|
{
|
||||||
|
ep.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
ep.CustomRating = request.CustomRating;
|
ep.CustomRating = request.CustomRating;
|
||||||
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
|
||||||
|
if (!ep.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
|
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
ep.OnMetadataChanged();
|
ep.OnMetadataChanged();
|
||||||
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -321,9 +348,18 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
foreach (BaseItem track in album.Children)
|
foreach (BaseItem track in album.Children)
|
||||||
{
|
{
|
||||||
track.OfficialRating = request.OfficialRating;
|
if (!track.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
|
{
|
||||||
|
track.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
track.CustomRating = request.CustomRating;
|
track.CustomRating = request.CustomRating;
|
||||||
track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
|
||||||
|
if (!track.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
|
track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
track.OnMetadataChanged();
|
track.OnMetadataChanged();
|
||||||
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||||||
public ActionResult UpdateLibraryOptions(
|
public ActionResult UpdateLibraryOptions(
|
||||||
[FromBody] UpdateLibraryOptionsDto request)
|
[FromBody] UpdateLibraryOptionsDto request)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById<CollectionFolder>(request.Id, User.GetUserId());
|
var item = _libraryManager.GetItemById<CollectionFolder>(request.Id);
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
.AddClientFields(User)
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
|
||||||
|
|
||||||
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
|
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
|
||||||
{
|
{
|
||||||
@@ -240,7 +241,7 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
return NotFound("No season exists with Id " + seasonId);
|
return NotFound("No season exists with Id " + seasonId);
|
||||||
}
|
}
|
||||||
|
|
||||||
episodes = seasonItem.GetEpisodes(user, dtoOptions);
|
episodes = seasonItem.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
else if (season.HasValue) // Season number was supplied. Get episodes by season number
|
else if (season.HasValue) // Season number was supplied. Get episodes by season number
|
||||||
{
|
{
|
||||||
@@ -256,7 +257,7 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
|
|
||||||
episodes = seasonItem is null ?
|
episodes = seasonItem is null ?
|
||||||
new List<BaseItem>()
|
new List<BaseItem>()
|
||||||
: ((Season)seasonItem).GetEpisodes(user, dtoOptions);
|
: ((Season)seasonItem).GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
else // No season number or season id was supplied. Returning all episodes.
|
else // No season number or season id was supplied. Returning all episodes.
|
||||||
{
|
{
|
||||||
@@ -265,7 +266,7 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
return NotFound("Series not found");
|
return NotFound("Series not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
episodes = series.GetEpisodes(user, dtoOptions).ToList();
|
episodes = series.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter after the fact in case the ui doesn't want them
|
// Filter after the fact in case the ui doesn't want them
|
||||||
|
|||||||
@@ -120,7 +120,12 @@ public static class RequestHelpers
|
|||||||
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
|
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
|
||||||
{
|
{
|
||||||
userId ??= httpContext.User.GetUserId();
|
userId ??= httpContext.User.GetUserId();
|
||||||
var user = userManager.GetUserById(userId.Value);
|
User? user = null;
|
||||||
|
if (!userId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
user = userManager.GetUserById(userId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
var session = await sessionManager.LogSessionActivity(
|
var session = await sessionManager.LogSessionActivity(
|
||||||
httpContext.User.GetClient(),
|
httpContext.User.GetClient(),
|
||||||
httpContext.User.GetVersion(),
|
httpContext.User.GetVersion(),
|
||||||
|
|||||||
@@ -142,6 +142,20 @@ public static class StreamingHelpers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
|
||||||
|
// Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate,
|
||||||
|
// which will cause the client to request extremely high bitrate that may fail the player/encoder
|
||||||
|
streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
|
||||||
|
|
||||||
|
if (streamingRequest.SegmentContainer is not null)
|
||||||
|
{
|
||||||
|
// Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
|
||||||
|
// Notably: Some channels won't play on FireFox and LG webOS
|
||||||
|
// Some channels from HDHomerun will experience A/V sync issues
|
||||||
|
streamingRequest.SegmentContainer = "ts";
|
||||||
|
streamingRequest.VideoCodec = "h264";
|
||||||
|
}
|
||||||
|
|
||||||
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
||||||
mediaSource = liveStreamInfo.Item1;
|
mediaSource = liveStreamInfo.Item1;
|
||||||
state.DirectStreamProvider = liveStreamInfo.Item2;
|
state.DirectStreamProvider = liveStreamInfo.Item2;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.9.1</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
||||||
.AsAsyncEnumerable();
|
.AsAsyncEnumerable();
|
||||||
|
|
||||||
if (userId.HasValue)
|
if (!userId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId.Value);
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
|
|||||||
@@ -16,21 +16,28 @@ public static class ServiceCollectionExtensions
|
|||||||
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
|
/// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
|
||||||
/// <returns>The updated service collection.</returns>
|
/// <returns>The updated service collection.</returns>
|
||||||
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
|
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
|
||||||
{
|
{
|
||||||
serviceCollection.AddEFSecondLevelCache(options =>
|
if (!disableSecondLevelCache)
|
||||||
options.UseMemoryCacheProvider()
|
{
|
||||||
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
|
serviceCollection.AddEFSecondLevelCache(options =>
|
||||||
.UseCacheKeyPrefix("EF_")
|
options.UseMemoryCacheProvider()
|
||||||
// Don't cache null values. Remove this optional setting if it's not necessary.
|
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
|
||||||
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
|
.UseCacheKeyPrefix("EF_")
|
||||||
|
// Don't cache null values. Remove this optional setting if it's not necessary.
|
||||||
|
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
||||||
{
|
{
|
||||||
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
||||||
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
|
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||||
.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
if (!disableSecondLevelCache)
|
||||||
|
{
|
||||||
|
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceCollection;
|
return serviceCollection;
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
||||||
|
|
||||||
var options = _config.Configuration.TrickplayOptions;
|
var options = _config.Configuration.TrickplayOptions;
|
||||||
|
if (options.Interval < 1000)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval);
|
||||||
|
options.Interval = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var width in options.WidthResolutions)
|
foreach (var width in options.WidthResolutions)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -121,6 +127,13 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mediaPath = mediaSource.Path;
|
||||||
|
if (!File.Exists(mediaPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Media not found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
||||||
var actualWidth = 2 * (width / 2);
|
var actualWidth = 2 * (width / 2);
|
||||||
|
|
||||||
@@ -139,7 +152,6 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaPath = mediaSource.Path;
|
|
||||||
var mediaStream = mediaSource.VideoStream;
|
var mediaStream = mediaSource.VideoStream;
|
||||||
var container = mediaSource.Container;
|
var container = mediaSource.Container;
|
||||||
|
|
||||||
@@ -261,7 +273,7 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update bitrate
|
// Update bitrate
|
||||||
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
|
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
|
||||||
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
|
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma warning disable CA1307
|
#pragma warning disable CA1307
|
||||||
#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -47,6 +47,8 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
|
private readonly IDictionary<Guid, User> _users;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -84,30 +86,29 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
||||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||||
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||||
|
|
||||||
|
_users = new ConcurrentDictionary<Guid, User>();
|
||||||
|
using var dbContext = _dbProvider.CreateDbContext();
|
||||||
|
foreach (var user in dbContext.Users
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(user => user.Permissions)
|
||||||
|
.Include(user => user.Preferences)
|
||||||
|
.Include(user => user.AccessSchedules)
|
||||||
|
.Include(user => user.ProfileImage)
|
||||||
|
.AsEnumerable())
|
||||||
|
{
|
||||||
|
_users.Add(user.Id, user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
|
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<User> Users
|
public IEnumerable<User> Users => _users.Values;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
|
||||||
return GetUsersInternal(dbContext).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<Guid> UsersIds
|
public IEnumerable<Guid> UsersIds => _users.Keys;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
|
||||||
return dbContext.Users.Select(u => u.Id).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||||
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||||
@@ -123,8 +124,8 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
_users.TryGetValue(id, out var user);
|
||||||
return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -135,9 +136,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Invalid username", nameof(name));
|
throw new ArgumentException("Invalid username", nameof(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
|
||||||
return GetUsersInternal(dbContext)
|
|
||||||
.FirstOrDefault(u => string.Equals(u.Username, name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -202,6 +201,8 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
user.AddDefaultPermissions();
|
user.AddDefaultPermissions();
|
||||||
user.AddDefaultPreferences();
|
user.AddDefaultPreferences();
|
||||||
|
|
||||||
|
_users.Add(user.Id, user);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,46 +237,40 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task DeleteUserAsync(Guid userId)
|
public async Task DeleteUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
if (!_users.TryGetValue(userId, out var user))
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException(nameof(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_users.Count == 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
|
||||||
|
user.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.HasPermission(PermissionKind.IsAdministrator)
|
||||||
|
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
|
||||||
|
user.Username),
|
||||||
|
nameof(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var user = await dbContext.Users
|
|
||||||
.AsSingleQuery()
|
|
||||||
.Include(u => u.Permissions)
|
|
||||||
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException(nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
|
|
||||||
user.Username));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.HasPermission(PermissionKind.IsAdministrator)
|
|
||||||
&& await dbContext.Users
|
|
||||||
.CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
|
|
||||||
.ConfigureAwait(false) == 1)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
|
|
||||||
user.Username),
|
|
||||||
nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
dbContext.Users.Remove(user);
|
dbContext.Users.Remove(user);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_users.Remove(userId);
|
||||||
|
|
||||||
|
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -542,23 +537,23 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
|
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||||
|
if (_users.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultName = Environment.UserName;
|
||||||
|
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
|
||||||
|
{
|
||||||
|
defaultName = "MyJellyfinUser";
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
||||||
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
|
||||||
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultName = Environment.UserName;
|
|
||||||
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
|
|
||||||
{
|
|
||||||
defaultName = "MyJellyfinUser";
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
|
||||||
|
|
||||||
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
|
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
|
||||||
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
||||||
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
||||||
@@ -605,9 +600,12 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var user = await GetUsersInternal(dbContext)
|
var user = dbContext.Users
|
||||||
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
|
.Include(u => u.Permissions)
|
||||||
.ConfigureAwait(false)
|
.Include(u => u.Preferences)
|
||||||
|
.Include(u => u.AccessSchedules)
|
||||||
|
.Include(u => u.ProfileImage)
|
||||||
|
.FirstOrDefault(u => u.Id.Equals(userId))
|
||||||
?? throw new ArgumentException("No user exists with given Id!");
|
?? throw new ArgumentException("No user exists with given Id!");
|
||||||
|
|
||||||
user.SubtitleMode = config.SubtitleMode;
|
user.SubtitleMode = config.SubtitleMode;
|
||||||
@@ -635,6 +633,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
|
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
|
||||||
|
|
||||||
dbContext.Update(user);
|
dbContext.Update(user);
|
||||||
|
_users[user.Id] = user;
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,9 +644,12 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var user = await GetUsersInternal(dbContext)
|
var user = dbContext.Users
|
||||||
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
|
.Include(u => u.Permissions)
|
||||||
.ConfigureAwait(false)
|
.Include(u => u.Preferences)
|
||||||
|
.Include(u => u.AccessSchedules)
|
||||||
|
.Include(u => u.ProfileImage)
|
||||||
|
.FirstOrDefault(u => u.Id.Equals(userId))
|
||||||
?? throw new ArgumentException("No user exists with given Id!");
|
?? throw new ArgumentException("No user exists with given Id!");
|
||||||
|
|
||||||
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
|
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
|
||||||
@@ -708,6 +710,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
|
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
|
||||||
|
|
||||||
dbContext.Update(user);
|
dbContext.Update(user);
|
||||||
|
_users[user.Id] = user;
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -728,6 +731,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.ProfileImage = null;
|
user.ProfileImage = null;
|
||||||
|
_users[user.Id] = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void ThrowIfInvalidUsername(string name)
|
internal static void ThrowIfInvalidUsername(string name)
|
||||||
@@ -874,15 +878,8 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
|
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
|
||||||
{
|
{
|
||||||
dbContext.Users.Update(user);
|
dbContext.Users.Update(user);
|
||||||
|
_users[user.Id] = user;
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext)
|
|
||||||
=> dbContext.Users
|
|
||||||
.AsSplitQuery()
|
|
||||||
.Include(user => user.Permissions)
|
|
||||||
.Include(user => user.Preferences)
|
|
||||||
.Include(user => user.AccessSchedules)
|
|
||||||
.Include(user => user.ProfileImage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,18 @@ public static class WebHostBuilderExtensions
|
|||||||
return builder
|
return builder
|
||||||
.UseKestrel((builderContext, options) =>
|
.UseKestrel((builderContext, options) =>
|
||||||
{
|
{
|
||||||
var addresses = appHost.NetManager.GetAllBindInterfaces(true);
|
var addresses = appHost.NetManager.GetAllBindInterfaces(false);
|
||||||
|
|
||||||
bool flagged = false;
|
bool flagged = false;
|
||||||
foreach (var netAdd in addresses)
|
foreach (var netAdd in addresses)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Kestrel is listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All IPv6 addresses" : netAdd.Address);
|
var address = netAdd.Address;
|
||||||
|
logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address);
|
||||||
options.Listen(netAdd.Address, appHost.HttpPort);
|
options.Listen(netAdd.Address, appHost.HttpPort);
|
||||||
if (appHost.ListenWithHttps)
|
if (appHost.ListenWithHttps)
|
||||||
{
|
{
|
||||||
options.Listen(
|
options.Listen(
|
||||||
netAdd.Address,
|
address,
|
||||||
appHost.HttpsPort,
|
appHost.HttpsPort,
|
||||||
listenOptions => listenOptions.UseHttps(appHost.Certificate));
|
listenOptions => listenOptions.UseHttps(appHost.Certificate));
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ public static class WebHostBuilderExtensions
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
options.Listen(
|
options.Listen(
|
||||||
netAdd.Address,
|
address,
|
||||||
appHost.HttpsPort,
|
appHost.HttpsPort,
|
||||||
listenOptions => listenOptions.UseHttps());
|
listenOptions => listenOptions.UseHttps());
|
||||||
}
|
}
|
||||||
@@ -84,6 +85,6 @@ public static class WebHostBuilderExtensions
|
|||||||
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.UseStartup(_ => new Startup(appHost));
|
.UseStartup(_ => new Startup(appHost, startupConfig));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ namespace Jellyfin.Server.Migrations
|
|||||||
typeof(Routines.FixPlaylistOwner),
|
typeof(Routines.FixPlaylistOwner),
|
||||||
typeof(Routines.MigrateRatingLevels),
|
typeof(Routines.MigrateRatingLevels),
|
||||||
typeof(Routines.AddDefaultCastReceivers),
|
typeof(Routines.AddDefaultCastReceivers),
|
||||||
typeof(Routines.UpdateDefaultPluginRepository)
|
typeof(Routines.UpdateDefaultPluginRepository),
|
||||||
|
typeof(Routines.FixAudioData),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
104
Jellyfin.Server/Migrations/Routines/FixAudioData.cs
Normal file
104
Jellyfin.Server/Migrations/Routines/FixAudioData.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fixes the data column of audio types to be deserializable.
|
||||||
|
/// </summary>
|
||||||
|
internal class FixAudioData : IMigrationRoutine
|
||||||
|
{
|
||||||
|
private const string DbFilename = "library.db";
|
||||||
|
private readonly ILogger<FixAudioData> _logger;
|
||||||
|
private readonly IServerApplicationPaths _applicationPaths;
|
||||||
|
private readonly IItemRepository _itemRepository;
|
||||||
|
|
||||||
|
public FixAudioData(
|
||||||
|
IServerApplicationPaths applicationPaths,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IItemRepository itemRepository)
|
||||||
|
{
|
||||||
|
_applicationPaths = applicationPaths;
|
||||||
|
_itemRepository = itemRepository;
|
||||||
|
_logger = loggerFactory.CreateLogger<FixAudioData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => "FixAudioData";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool PerformOnNewInstall => false;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Perform()
|
||||||
|
{
|
||||||
|
var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
|
||||||
|
|
||||||
|
// Back up the database before modifying any entries
|
||||||
|
for (int i = 1; ; i++)
|
||||||
|
{
|
||||||
|
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
|
||||||
|
if (!File.Exists(bakPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(dbPath, bakPath);
|
||||||
|
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Backfilling audio lyrics data to database.");
|
||||||
|
var startIndex = 0;
|
||||||
|
var records = _itemRepository.GetCount(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Audio],
|
||||||
|
});
|
||||||
|
|
||||||
|
while (startIndex < records)
|
||||||
|
{
|
||||||
|
var results = _itemRepository.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Audio],
|
||||||
|
StartIndex = startIndex,
|
||||||
|
Limit = 100,
|
||||||
|
SkipDeserialization = true
|
||||||
|
})
|
||||||
|
.Cast<Audio>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var audio in results)
|
||||||
|
{
|
||||||
|
var lyricMediaStreams = audio.GetMediaStreams().Where(s => s.Type == MediaStreamType.Lyric).Select(s => s.Path).ToList();
|
||||||
|
if (lyricMediaStreams.Count > 0)
|
||||||
|
{
|
||||||
|
audio.HasLyrics = true;
|
||||||
|
audio.LyricFiles = lyricMediaStreams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_itemRepository.SaveItems(results, CancellationToken.None);
|
||||||
|
startIndex += 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||||
{
|
{
|
||||||
connection.Open();
|
connection.Open();
|
||||||
var dbContext = _provider.CreateDbContext();
|
using var dbContext = _provider.CreateDbContext();
|
||||||
|
|
||||||
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
||||||
|
|
||||||
|
|||||||
@@ -40,15 +40,18 @@ namespace Jellyfin.Server
|
|||||||
{
|
{
|
||||||
private readonly CoreAppHost _serverApplicationHost;
|
private readonly CoreAppHost _serverApplicationHost;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Startup" /> class.
|
/// Initializes a new instance of the <see cref="Startup" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appHost">The server application host.</param>
|
/// <param name="appHost">The server application host.</param>
|
||||||
public Startup(CoreAppHost appHost)
|
/// <param name="startupConfig">The server startupConfig.</param>
|
||||||
|
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
|
||||||
{
|
{
|
||||||
_serverApplicationHost = appHost;
|
_serverApplicationHost = appHost;
|
||||||
_serverConfigurationManager = appHost.ConfigurationManager;
|
_serverConfigurationManager = appHost.ConfigurationManager;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -67,7 +70,7 @@ namespace Jellyfin.Server
|
|||||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||||
services.AddJellyfinDbContext();
|
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
|
||||||
services.AddJellyfinApiSwagger();
|
services.AddJellyfinApiSwagger();
|
||||||
|
|
||||||
// configure custom legacy authentication
|
// configure custom legacy authentication
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.9.1</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ public static class NetworkConstants
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
|
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IPv4 Link-Local as defined in RFC 3927.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly IPNetwork IPv4RFC3927LinkLocal = new IPNetwork(IPAddress.Parse("169.254.0.0"), 16);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// IPv6 loopback as defined in RFC 4291.
|
/// IPv6 loopback as defined in RFC 4291.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -169,8 +169,7 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
|
|
||||||
var childUpdateType = ItemUpdateType.None;
|
var childUpdateType = ItemUpdateType.None;
|
||||||
|
|
||||||
// Refresh songs only and not m3u files in album folder
|
foreach (var item in items)
|
||||||
foreach (var item in items.OfType<Audio>())
|
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|||||||
@@ -751,9 +751,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual bool SupportsAncestors => true;
|
public virtual bool SupportsAncestors => true;
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public virtual bool StopRefreshIfLocalMetadataFound => true;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
|
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
TrailerTypes = Array.Empty<TrailerType>();
|
TrailerTypes = Array.Empty<TrailerType>();
|
||||||
VideoTypes = Array.Empty<VideoType>();
|
VideoTypes = Array.Empty<VideoType>();
|
||||||
Years = Array.Empty<int>();
|
Years = Array.Empty<int>();
|
||||||
|
SkipDeserialization = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InternalItemsQuery(User? user)
|
public InternalItemsQuery(User? user)
|
||||||
@@ -358,6 +359,8 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public string? SeriesTimerId { get; set; }
|
public string? SeriesTimerId { get; set; }
|
||||||
|
|
||||||
|
public bool SkipDeserialization { get; set; }
|
||||||
|
|
||||||
public void SetUser(User user)
|
public void SetUser(User user)
|
||||||
{
|
{
|
||||||
MaxParentalRating = user.MaxParentalAgeRating;
|
MaxParentalRating = user.MaxParentalAgeRating;
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||||||
set => TmdbCollectionName = value;
|
set => TmdbCollectionName = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool StopRefreshIfLocalMetadataFound => false;
|
|
||||||
|
|
||||||
public override double GetDefaultPrimaryImageAspectRatio()
|
public override double GetDefaultPrimaryImageAspectRatio()
|
||||||
{
|
{
|
||||||
// hack for tv plugins
|
// hack for tv plugins
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
||||||
|
|
||||||
var items = GetEpisodes(user, query.DtoOptions).Where(filter);
|
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
|
||||||
|
|
||||||
return PostFilterAndSort(items, query, false);
|
return PostFilterAndSort(items, query, false);
|
||||||
}
|
}
|
||||||
@@ -169,30 +169,31 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <param name="options">The options to use.</param>
|
/// <param name="options">The options to use.</param>
|
||||||
|
/// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
|
||||||
/// <returns>Set of episodes.</returns>
|
/// <returns>Set of episodes.</returns>
|
||||||
public List<BaseItem> GetEpisodes(User user, DtoOptions options)
|
public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
return GetEpisodes(Series, user, options);
|
return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options)
|
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
return GetEpisodes(series, user, null, options);
|
return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options)
|
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options);
|
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes()
|
public List<BaseItem> GetEpisodes()
|
||||||
{
|
{
|
||||||
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true));
|
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
return GetEpisodes(user, new DtoOptions(true));
|
return GetEpisodes(user, new DtoOptions(true), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool GetBlockUnratedValue(User user)
|
protected override bool GetBlockUnratedValue(User user)
|
||||||
|
|||||||
@@ -28,16 +28,12 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
public Series()
|
public Series()
|
||||||
{
|
{
|
||||||
AirDays = Array.Empty<DayOfWeek>();
|
AirDays = Array.Empty<DayOfWeek>();
|
||||||
SeasonNames = new Dictionary<int, string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DayOfWeek[] AirDays { get; set; }
|
public DayOfWeek[] AirDays { get; set; }
|
||||||
|
|
||||||
public string AirTime { get; set; }
|
public string AirTime { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Dictionary<int, string> SeasonNames { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override bool SupportsAddingToPlaylist => true;
|
public override bool SupportsAddingToPlaylist => true;
|
||||||
|
|
||||||
@@ -73,9 +69,6 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
/// <value>The status.</value>
|
/// <value>The status.</value>
|
||||||
public SeriesStatus? Status { get; set; }
|
public SeriesStatus? Status { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool StopRefreshIfLocalMetadataFound => false;
|
|
||||||
|
|
||||||
public override double GetDefaultPrimaryImageAspectRatio()
|
public override double GetDefaultPrimaryImageAspectRatio()
|
||||||
{
|
{
|
||||||
double value = 2;
|
double value = 2;
|
||||||
@@ -257,7 +250,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
return LibraryManager.GetItemsResult(query);
|
return LibraryManager.GetItemsResult(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
|
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
var seriesKey = GetUniqueSeriesKey(this);
|
var seriesKey = GetUniqueSeriesKey(this);
|
||||||
|
|
||||||
@@ -267,10 +260,10 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
|
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||||
DtoOptions = options
|
DtoOptions = options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (user is null || !user.DisplayMissingEpisodes)
|
if (!shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
query.IsMissing = false;
|
query.IsMissing = false;
|
||||||
}
|
}
|
||||||
@@ -280,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
|
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
|
||||||
|
|
||||||
var allEpisodes = allItems.OfType<Season>()
|
var allEpisodes = allItems.OfType<Season>()
|
||||||
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
|
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
|
||||||
.Reverse();
|
.Reverse();
|
||||||
|
|
||||||
// Specials could appear twice based on above - once in season 0, once in the aired season
|
// Specials could appear twice based on above - once in season 0, once in the aired season
|
||||||
@@ -292,8 +285,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Refresh bottom up, children first, then the boxset
|
// Refresh bottom up, seasons and episodes first, then the series
|
||||||
// By then hopefully the movies within will have Tmdb collection values
|
|
||||||
var items = GetRecursiveChildren();
|
var items = GetRecursiveChildren();
|
||||||
|
|
||||||
var totalItems = items.Count;
|
var totalItems = items.Count;
|
||||||
@@ -356,7 +348,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
|
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
||||||
|
|
||||||
@@ -373,24 +365,22 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
if (user is not null)
|
|
||||||
|
if (!shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
if (!user.DisplayMissingEpisodes)
|
query.IsMissing = false;
|
||||||
{
|
|
||||||
query.IsMissing = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var allItems = LibraryManager.GetItemList(query);
|
var allItems = LibraryManager.GetItemList(query);
|
||||||
|
|
||||||
return GetSeasonEpisodes(parentSeason, user, allItems, options);
|
return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
|
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
if (allSeriesEpisodes is null)
|
if (allSeriesEpisodes is null)
|
||||||
{
|
{
|
||||||
return GetSeasonEpisodes(parentSeason, user, options);
|
return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
|
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
TrailerTypes = Array.Empty<TrailerType>();
|
TrailerTypes = Array.Empty<TrailerType>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool StopRefreshIfLocalMetadataFound => false;
|
|
||||||
|
|
||||||
public TrailerType[] TrailerTypes { get; set; }
|
public TrailerType[] TrailerTypes { get; set; }
|
||||||
|
|
||||||
public override double GetDefaultPrimaryImageAspectRatio()
|
public override double GetDefaultPrimaryImageAspectRatio()
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable second level cache of sqlite.
|
||||||
|
/// </summary>
|
||||||
|
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -128,5 +133,15 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// <returns>The sqlite cache size.</returns>
|
/// <returns>The sqlite cache size.</returns>
|
||||||
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
||||||
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
|
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">The configuration to read the setting from.</param>
|
||||||
|
/// <returns>Whether second level cache disabled.</returns>
|
||||||
|
public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.9.1</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
||||||
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
||||||
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
|
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
|
||||||
|
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
|
||||||
|
|
||||||
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||||
@@ -680,16 +681,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetInputPathArgument(EncodingJobInfo state)
|
|
||||||
{
|
|
||||||
return state.MediaSource.VideoType switch
|
|
||||||
{
|
|
||||||
VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
|
|
||||||
VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
|
|
||||||
_ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the audio encoder.
|
/// Gets the audio encoder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1005,7 +996,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
|
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
|
||||||
|
|
||||||
if (IsVulkanFullSupported()
|
if (IsVulkanFullSupported()
|
||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
|
||||||
|
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||||
{
|
{
|
||||||
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
||||||
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
||||||
@@ -1197,13 +1189,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
|
var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
|
||||||
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
|
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
|
||||||
arg.Append(" -f concat -safe 0 -i ")
|
arg.Append(" -f concat -safe 0 -i \"")
|
||||||
.Append(tmpConcatPath);
|
.Append(tmpConcatPath)
|
||||||
|
.Append("\" ");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
arg.Append(" -i ")
|
arg.Append(" -i ")
|
||||||
.Append(GetInputPathArgument(state));
|
.Append(_mediaEncoder.GetInputPathArgument(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
// sub2video for external graphical subtitles
|
// sub2video for external graphical subtitles
|
||||||
@@ -2083,6 +2076,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
profile = "constrained_high";
|
profile = "constrained_high";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
profile = "constrained_baseline";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
profile = "constrained_high";
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profile))
|
if (!string.IsNullOrEmpty(profile))
|
||||||
{
|
{
|
||||||
// Currently there's no profile option in av1_nvenc encoder
|
// Currently there's no profile option in av1_nvenc encoder
|
||||||
@@ -2316,7 +2321,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
if (request.VideoBitRate.HasValue
|
if (request.VideoBitRate.HasValue
|
||||||
&& (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value))
|
&& (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value))
|
||||||
{
|
{
|
||||||
return false;
|
// For LiveTV that has no bitrate, let's try copy if other conditions are met
|
||||||
|
if (string.IsNullOrWhiteSpace(request.LiveStreamId) || videoStream.BitRate.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
|
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
|
||||||
@@ -2629,10 +2638,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& state.AudioStream.Channels.HasValue
|
&& state.AudioStream.Channels.HasValue
|
||||||
&& state.AudioStream.Channels.Value == 6)
|
&& state.AudioStream.Channels.Value == 6)
|
||||||
{
|
{
|
||||||
|
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
||||||
|
{
|
||||||
|
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
switch (encodingOptions.DownMixStereoAlgorithm)
|
switch (encodingOptions.DownMixStereoAlgorithm)
|
||||||
{
|
{
|
||||||
case DownMixStereoAlgorithms.Dave750:
|
case DownMixStereoAlgorithms.Dave750:
|
||||||
filters.Add("volume=4.25");
|
|
||||||
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
|
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
|
||||||
break;
|
break;
|
||||||
case DownMixStereoAlgorithms.NightmodeDialogue:
|
case DownMixStereoAlgorithms.NightmodeDialogue:
|
||||||
@@ -2640,11 +2653,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
break;
|
break;
|
||||||
case DownMixStereoAlgorithms.None:
|
case DownMixStereoAlgorithms.None:
|
||||||
default:
|
default:
|
||||||
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
|
||||||
{
|
|
||||||
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2771,7 +2779,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (time > 0)
|
if (time > 0)
|
||||||
{
|
{
|
||||||
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(time));
|
// For direct streaming/remuxing, we seek at the exact position of the keyframe
|
||||||
|
// However, ffmpeg will seek to previous keyframe when the exact time is the input
|
||||||
|
// Workaround this by adding 0.5s offset to the seeking time to get the exact keyframe on most videos.
|
||||||
|
// This will help subtitle syncing.
|
||||||
|
var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec);
|
||||||
|
var seekTick = isHlsRemuxing ? time + 5000000L : time;
|
||||||
|
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
|
||||||
|
|
||||||
if (state.IsVideoRequest)
|
if (state.IsVideoRequest)
|
||||||
{
|
{
|
||||||
@@ -3155,7 +3169,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
int? requestedMaxHeight)
|
int? requestedMaxHeight)
|
||||||
{
|
{
|
||||||
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
|
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isMjpeg = videoEncoder is not null && videoEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
|
||||||
var scaleVal = isV4l2 ? 64 : 2;
|
var scaleVal = isV4l2 ? 64 : 2;
|
||||||
|
var targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder
|
||||||
|
|
||||||
// If fixed dimensions were supplied
|
// If fixed dimensions were supplied
|
||||||
if (requestedWidth.HasValue && requestedHeight.HasValue)
|
if (requestedWidth.HasValue && requestedHeight.HasValue)
|
||||||
@@ -3184,10 +3200,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2",
|
@"scale=trunc(min(max(iw\,ih*{3})\,min({0}\,{1}*{3}))/{2})*{2}:trunc(min(max(iw/{3}\,ih)\,min({0}/{3}\,{1}))/2)*2",
|
||||||
maxWidthParam,
|
maxWidthParam,
|
||||||
maxHeightParam,
|
maxHeightParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a fixed width was requested
|
// If a fixed width was requested
|
||||||
@@ -3203,8 +3220,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"scale={0}:trunc(ow/a/2)*2",
|
"scale={0}:trunc(ow/{1}/2)*2",
|
||||||
widthParam);
|
widthParam,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a fixed height was requested
|
// If a fixed height was requested
|
||||||
@@ -3214,9 +3232,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"scale=trunc(oh*a/{1})*{1}:{0}",
|
"scale=trunc(oh*{2}/{1})*{1}:{0}",
|
||||||
heightParam,
|
heightParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a max width was requested
|
// If a max width was requested
|
||||||
@@ -3226,9 +3245,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2",
|
@"scale=trunc(min(max(iw\,ih*{2})\,{0})/{1})*{1}:trunc(ow/{2}/2)*2",
|
||||||
maxWidthParam,
|
maxWidthParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a max height was requested
|
// If a max height was requested
|
||||||
@@ -3238,9 +3258,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})",
|
@"scale=trunc(oh*{2}/{1})*{1}:min(max(iw/{2}\,ih)\,{0})",
|
||||||
maxHeightParam,
|
maxHeightParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
@@ -4285,6 +4306,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// map from qsv to vaapi.
|
// map from qsv to vaapi.
|
||||||
mainFilters.Add("hwmap=derive_device=vaapi");
|
mainFilters.Add("hwmap=derive_device=vaapi");
|
||||||
|
mainFilters.Add("format=vaapi");
|
||||||
}
|
}
|
||||||
|
|
||||||
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
|
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
|
||||||
@@ -4294,6 +4316,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// map from vaapi to qsv.
|
// map from vaapi to qsv.
|
||||||
mainFilters.Add("hwmap=derive_device=qsv");
|
mainFilters.Add("hwmap=derive_device=qsv");
|
||||||
|
mainFilters.Add("format=qsv");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4468,7 +4491,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// prefered vaapi + vulkan filters pipeline
|
// prefered vaapi + vulkan filters pipeline
|
||||||
if (_mediaEncoder.IsVaapiDeviceAmd
|
if (_mediaEncoder.IsVaapiDeviceAmd
|
||||||
&& isVaapiVkSupported
|
&& isVaapiVkSupported
|
||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
|
||||||
|
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||||
{
|
{
|
||||||
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
|
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
|
||||||
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
||||||
|
|||||||
@@ -245,6 +245,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <returns>A playlist.</returns>
|
/// <returns>A playlist.</returns>
|
||||||
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
|
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the input path argument from <see cref="EncodingJobInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">The <see cref="EncodingJobInfo"/>.</param>
|
||||||
|
/// <returns>The input path argument.</returns>
|
||||||
|
string GetInputPathArgument(EncodingJobInfo state);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the input path argument.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The item path.</param>
|
||||||
|
/// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
|
||||||
|
/// <returns>The input path argument.</returns>
|
||||||
|
string GetInputPathArgument(string path, MediaSourceInfo mediaSource);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a FFmpeg concat config for the source.
|
/// Generates a FFmpeg concat config for the source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
public ItemInfo(BaseItem item)
|
public ItemInfo(BaseItem item)
|
||||||
{
|
{
|
||||||
Path = item.Path;
|
Path = item.Path;
|
||||||
|
ParentId = item.ParentId;
|
||||||
|
IndexNumber = item.IndexNumber;
|
||||||
ContainingFolderPath = item.ContainingFolderPath;
|
ContainingFolderPath = item.ContainingFolderPath;
|
||||||
IsInMixedFolder = item.IsInMixedFolder;
|
IsInMixedFolder = item.IsInMixedFolder;
|
||||||
|
|
||||||
@@ -27,6 +29,10 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
|
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
public Guid ParentId { get; set; }
|
||||||
|
|
||||||
|
public int? IndexNumber { get; set; }
|
||||||
|
|
||||||
public string ContainingFolderPath { get; set; }
|
public string ContainingFolderPath { get; set; }
|
||||||
|
|
||||||
public VideoType VideoType { get; set; }
|
public VideoType VideoType { get; set; }
|
||||||
|
|||||||
@@ -89,15 +89,28 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
string outputPath,
|
string outputPath,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (shouldExtractOneByOne)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(outputPath))
|
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||||
|
foreach (var i in attachmentIndexes)
|
||||||
{
|
{
|
||||||
await ExtractAllAttachmentsInternal(
|
var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
|
||||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
|
||||||
outputPath,
|
}
|
||||||
false,
|
}
|
||||||
cancellationToken).ConfigureAwait(false);
|
else
|
||||||
|
{
|
||||||
|
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(outputPath))
|
||||||
|
{
|
||||||
|
await ExtractAllAttachmentsInternal(
|
||||||
|
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||||
|
outputPath,
|
||||||
|
false,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using static Nikse.SubtitleEdit.Core.Common.IfoParser;
|
|
||||||
|
|
||||||
namespace MediaBrowser.MediaEncoding.Encoder
|
namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
{
|
{
|
||||||
@@ -458,9 +456,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
extraArgs += " -probesize " + ffmpegProbeSize;
|
extraArgs += " -probesize " + ffmpegProbeSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent))
|
if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
|
||||||
{
|
{
|
||||||
extraArgs += " -user_agent " + userAgent;
|
extraArgs += $" -user_agent \"{userAgent}\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
|
if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
|
||||||
@@ -621,7 +619,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
ImageFormat? targetFormat,
|
ImageFormat? targetFormat,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var inputArgument = GetInputArgument(inputFile, mediaSource);
|
var inputArgument = GetInputPathArgument(inputFile, mediaSource);
|
||||||
|
|
||||||
if (!isAudio)
|
if (!isAudio)
|
||||||
{
|
{
|
||||||
@@ -824,6 +822,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
options.EnableTonemapping = false;
|
options.EnableTonemapping = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.AspectRatio))
|
||||||
|
{
|
||||||
|
// For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimensions
|
||||||
|
var darParts = imageStream.AspectRatio.Split(':');
|
||||||
|
var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], CultureInfo.InvariantCulture));
|
||||||
|
// When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
|
||||||
|
// Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, but we really can do little about it.
|
||||||
|
var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05;
|
||||||
|
if (shouldResetHeight)
|
||||||
|
{
|
||||||
|
// SAR = DAR * Height / Width
|
||||||
|
// RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
|
||||||
|
imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
|
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
|
||||||
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
|
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
|
||||||
{
|
{
|
||||||
@@ -871,6 +885,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
throw new InvalidOperationException("Empty or invalid input argument.");
|
throw new InvalidOperationException("Empty or invalid input argument.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float? encoderQuality = qualityScale;
|
||||||
|
if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// vaapi's mjpeg encoder uses jpeg quality divided by QP2LAMBDA (118) as input, instead of ffmpeg defined qscale
|
||||||
|
// ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
|
||||||
|
// jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
|
||||||
|
encoderQuality = (100 - ((qualityScale - 1) * (100 / 30))) / 118;
|
||||||
|
}
|
||||||
|
|
||||||
// Output arguments
|
// Output arguments
|
||||||
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
|
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(targetDirectory);
|
Directory.CreateDirectory(targetDirectory);
|
||||||
@@ -884,7 +907,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
filterParam,
|
filterParam,
|
||||||
outputThreads.GetValueOrDefault(_threads),
|
outputThreads.GetValueOrDefault(_threads),
|
||||||
vidEncoder,
|
vidEncoder,
|
||||||
qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
|
qualityScale.HasValue ? "-qscale:v " + encoderQuality.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
|
||||||
"image2",
|
"image2",
|
||||||
outputPath);
|
outputPath);
|
||||||
|
|
||||||
@@ -936,7 +959,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
|
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||||
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
|
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
|
||||||
|
|
||||||
while (isResponsive)
|
while (isResponsive && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -950,8 +973,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
|
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
|
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
|
||||||
|
|
||||||
isResponsive = jpegCount > lastCount;
|
isResponsive = jpegCount > lastCount;
|
||||||
@@ -960,7 +981,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
if (!ranToCompletion)
|
if (!ranToCompletion)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Stopping trickplay extraction due to process inactivity.");
|
if (!isResponsive)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Trickplay process unresponsive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Stopping trickplay extraction.");
|
||||||
StopProcess(processWrapper, 1000);
|
StopProcess(processWrapper, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1128,16 +1154,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
|
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
|
||||||
|
|
||||||
// Get all files from the BDMV/STREAMING directory
|
// Get all files from the BDMV/STREAMING directory
|
||||||
var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
|
|
||||||
|
|
||||||
// Only return playable local .m2ts files
|
// Only return playable local .m2ts files
|
||||||
return directoryFiles
|
return validPlaybackFiles
|
||||||
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
|
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
|
||||||
|
.Where(f => f.Exists)
|
||||||
.Select(f => f.FullName)
|
.Select(f => f.FullName)
|
||||||
.Order()
|
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetInputPathArgument(EncodingJobInfo state)
|
||||||
|
=> GetInputPathArgument(state.MediaPath, state.MediaSource);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
|
||||||
|
{
|
||||||
|
return mediaSource.VideoType switch
|
||||||
|
{
|
||||||
|
VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null).ToList(), mediaSource),
|
||||||
|
VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path).ToList(), mediaSource),
|
||||||
|
_ => GetInputArgument(path, mediaSource)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
|
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -267,14 +267,14 @@ namespace MediaBrowser.Model.Entities
|
|||||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase) && !string.Equals(Codec, "dts", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
attributes.Add(Profile);
|
attributes.Add(Profile);
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(Codec))
|
||||||
|
{
|
||||||
|
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ChannelLayout))
|
if (!string.IsNullOrEmpty(ChannelLayout))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.9.1</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -33,7 +33,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<PackageReference Include="MimeTypes">
|
<PackageReference Include="MimeTypes">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace MediaBrowser.Model.Search
|
|||||||
/// Gets or sets the matched term.
|
/// Gets or sets the matched term.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The matched term.</value>
|
/// <value>The matched term.</value>
|
||||||
public string MatchedTerm { get; set; }
|
public string? MatchedTerm { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the index number.
|
/// Gets or sets the index number.
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -122,7 +121,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var metadataResult = new MetadataResult<TItemType>
|
var metadataResult = new MetadataResult<TItemType>
|
||||||
{
|
{
|
||||||
Item = itemOfType
|
Item = itemOfType,
|
||||||
|
People = LibraryManager.GetPeople(item)
|
||||||
};
|
};
|
||||||
|
|
||||||
bool hasRefreshedMetadata = true;
|
bool hasRefreshedMetadata = true;
|
||||||
@@ -165,7 +165,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next run remote image providers, but only if local image providers didn't throw an exception
|
// Next run remote image providers, but only if local image providers didn't throw an exception
|
||||||
if (!localImagesFailed && refreshOptions.ImageRefreshMode != MetadataRefreshMode.ValidationOnly)
|
if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
|
||||||
{
|
{
|
||||||
var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
|
var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
|
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (result.Item.SupportsPeople && result.People is not null)
|
if (result.Item.SupportsPeople)
|
||||||
{
|
{
|
||||||
var baseItem = result.Item;
|
var baseItem = result.Item;
|
||||||
|
|
||||||
@@ -399,7 +399,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
if (!child.IsFolder)
|
// Exclude any folders and virtual items since they are only placeholders
|
||||||
|
if (!child.IsFolder && !child.IsVirtualItem)
|
||||||
{
|
{
|
||||||
var childDateCreated = child.DateCreated;
|
var childDateCreated = child.DateCreated;
|
||||||
if (childDateCreated > dateLastMediaAdded)
|
if (childDateCreated > dateLastMediaAdded)
|
||||||
@@ -655,26 +656,19 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
|
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.IsLocked)
|
||||||
|
{
|
||||||
|
return refreshResult;
|
||||||
|
}
|
||||||
|
|
||||||
var temp = new MetadataResult<TItemType>
|
var temp = new MetadataResult<TItemType>
|
||||||
{
|
{
|
||||||
Item = CreateNew()
|
Item = CreateNew()
|
||||||
};
|
};
|
||||||
temp.Item.Path = item.Path;
|
temp.Item.Path = item.Path;
|
||||||
|
temp.Item.Id = item.Id;
|
||||||
|
|
||||||
// If replacing all metadata, run internet providers first
|
|
||||||
if (options.ReplaceAllMetadata)
|
|
||||||
{
|
|
||||||
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
refreshResult.UpdateType |= remoteResult.UpdateType;
|
|
||||||
refreshResult.ErrorMessage = remoteResult.ErrorMessage;
|
|
||||||
refreshResult.Failures += remoteResult.Failures;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasLocalMetadata = false;
|
|
||||||
var foundImageTypes = new List<ImageType>();
|
var foundImageTypes = new List<ImageType>();
|
||||||
|
|
||||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var providerName = provider.GetType().Name;
|
||||||
@@ -720,15 +714,9 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
// Only one local provider allowed per item
|
|
||||||
if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
|
|
||||||
{
|
|
||||||
hasLocalMetadata = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,10 +735,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local metadata is king - if any is found don't run remote providers
|
var isLocalLocked = temp.Item.IsLocked;
|
||||||
if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !item.StopRefreshIfLocalMetadataFound))
|
if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
|
||||||
{
|
{
|
||||||
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
|
var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
refreshResult.UpdateType |= remoteResult.UpdateType;
|
refreshResult.UpdateType |= remoteResult.UpdateType;
|
||||||
@@ -762,19 +750,20 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
if (refreshResult.UpdateType > ItemUpdateType.None)
|
if (refreshResult.UpdateType > ItemUpdateType.None)
|
||||||
{
|
{
|
||||||
if (hasLocalMetadata)
|
if (!options.RemoveOldMetadata)
|
||||||
|
{
|
||||||
|
// Add existing metadata to provider result if it does not exist there
|
||||||
|
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalLocked)
|
||||||
{
|
{
|
||||||
MergeData(temp, metadata, item.LockedFields, true, true);
|
MergeData(temp, metadata, item.LockedFields, true, true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!options.RemoveOldMetadata)
|
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
|
||||||
{
|
MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
|
||||||
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will always replace all metadata when Scan for new and updated files is used. Else, follow the options.
|
|
||||||
MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -787,16 +776,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
return refreshResult;
|
return refreshResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual bool IsFullLocalMetadata(TItemType item)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(item.Name))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
|
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
|
Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
|
||||||
@@ -821,7 +800,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
return new TItemType();
|
return new TItemType();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
|
private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var refreshResult = new RefreshResult();
|
var refreshResult = new RefreshResult();
|
||||||
|
|
||||||
@@ -846,7 +825,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
result.Provider = provider.Name;
|
result.Provider = provider.Name;
|
||||||
|
|
||||||
MergeData(result, temp, Array.Empty<MetadataField>(), false, false);
|
MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false);
|
||||||
MergeNewData(temp.Item, id);
|
MergeNewData(temp.Item, id);
|
||||||
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
|
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
|
||||||
@@ -949,11 +928,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
|
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
|
||||||
{
|
{
|
||||||
// Safeguard against incoming data having an empty name
|
target.OriginalTitle = source.OriginalTitle;
|
||||||
if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
|
|
||||||
{
|
|
||||||
target.OriginalTitle = source.OriginalTitle;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaceData || !target.CommunityRating.HasValue)
|
if (replaceData || !target.CommunityRating.HasValue)
|
||||||
@@ -1016,7 +991,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
targetResult.People = sourceResult.People;
|
targetResult.People = sourceResult.People;
|
||||||
}
|
}
|
||||||
else if (targetResult.People is not null && sourceResult.People is not null)
|
else if (sourceResult.People is not null && sourceResult.People.Count >= 0)
|
||||||
{
|
{
|
||||||
MergePeople(sourceResult.People, targetResult.People);
|
MergePeople(sourceResult.People, targetResult.People);
|
||||||
}
|
}
|
||||||
@@ -1049,6 +1024,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
target.Studios = source.Studios;
|
target.Studios = source.Studios;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lockedFields.Contains(MetadataField.Tags))
|
if (!lockedFields.Contains(MetadataField.Tags))
|
||||||
@@ -1057,6 +1036,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
target.Tags = source.Tags;
|
target.Tags = source.Tags;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lockedFields.Contains(MetadataField.ProductionLocations))
|
if (!lockedFields.Contains(MetadataField.ProductionLocations))
|
||||||
@@ -1065,6 +1048,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
target.ProductionLocations = source.ProductionLocations;
|
target.ProductionLocations = source.ProductionLocations;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var id in source.ProviderIds)
|
foreach (var id in source.ProviderIds)
|
||||||
@@ -1082,17 +1069,28 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (replaceData || !target.CriticRating.HasValue)
|
||||||
|
{
|
||||||
|
target.CriticRating = source.CriticRating;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceData || target.RemoteTrailers.Count == 0)
|
||||||
|
{
|
||||||
|
target.RemoteTrailers = source.RemoteTrailers;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
MergeAlbumArtist(source, target, replaceData);
|
MergeAlbumArtist(source, target, replaceData);
|
||||||
MergeCriticRating(source, target, replaceData);
|
|
||||||
MergeTrailers(source, target, replaceData);
|
|
||||||
MergeVideoInfo(source, target, replaceData);
|
MergeVideoInfo(source, target, replaceData);
|
||||||
MergeDisplayOrder(source, target, replaceData);
|
MergeDisplayOrder(source, target, replaceData);
|
||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
|
if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
|
||||||
{
|
{
|
||||||
var forcedSortName = source.ForcedSortName;
|
var forcedSortName = source.ForcedSortName;
|
||||||
|
if (!string.IsNullOrEmpty(forcedSortName))
|
||||||
if (!string.IsNullOrWhiteSpace(forcedSortName))
|
|
||||||
{
|
{
|
||||||
target.ForcedSortName = forcedSortName;
|
target.ForcedSortName = forcedSortName;
|
||||||
}
|
}
|
||||||
@@ -1100,22 +1098,44 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
if (mergeMetadataSettings)
|
if (mergeMetadataSettings)
|
||||||
{
|
{
|
||||||
target.LockedFields = source.LockedFields;
|
if (replaceData || !target.IsLocked)
|
||||||
target.IsLocked = source.IsLocked;
|
{
|
||||||
|
target.IsLocked = target.IsLocked || source.IsLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.LockedFields.Length == 0)
|
||||||
|
{
|
||||||
|
target.LockedFields = source.LockedFields;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the value if it's there, but if not then don't overwrite with the default
|
|
||||||
if (source.DateCreated != default)
|
if (source.DateCreated != default)
|
||||||
{
|
{
|
||||||
target.DateCreated = source.DateCreated;
|
target.DateCreated = source.DateCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
|
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
|
||||||
target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
|
{
|
||||||
|
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
|
||||||
|
{
|
||||||
|
target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
||||||
{
|
{
|
||||||
|
if (target is null)
|
||||||
|
{
|
||||||
|
target = new List<PersonInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var person in target)
|
foreach (var person in target)
|
||||||
{
|
{
|
||||||
var normalizedName = person.Name.RemoveDiacritics();
|
var normalizedName = person.Name.RemoveDiacritics();
|
||||||
@@ -1144,7 +1164,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
|
if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
|
||||||
{
|
{
|
||||||
var displayOrder = sourceHasDisplayOrder.DisplayOrder;
|
var displayOrder = sourceHasDisplayOrder.DisplayOrder;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(displayOrder))
|
if (!string.IsNullOrWhiteSpace(displayOrder))
|
||||||
{
|
{
|
||||||
targetHasDisplayOrder.DisplayOrder = displayOrder;
|
targetHasDisplayOrder.DisplayOrder = displayOrder;
|
||||||
@@ -1162,22 +1181,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
|
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
|
||||||
}
|
}
|
||||||
}
|
else if (sourceHasAlbumArtist.AlbumArtists.Count >= 0)
|
||||||
}
|
{
|
||||||
|
targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray();
|
||||||
private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
|
}
|
||||||
{
|
|
||||||
if (replaceData || !target.CriticRating.HasValue)
|
|
||||||
{
|
|
||||||
target.CriticRating = source.CriticRating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
|
|
||||||
{
|
|
||||||
if (replaceData || target.RemoteTrailers.Count == 0)
|
|
||||||
{
|
|
||||||
target.RemoteTrailers = source.RemoteTrailers;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,7 +1192,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
if (source is Video sourceCast && target is Video targetCast)
|
if (source is Video sourceCast && target is Video targetCast)
|
||||||
{
|
{
|
||||||
if (replaceData || targetCast.Video3DFormat is null)
|
if (replaceData || !targetCast.Video3DFormat.HasValue)
|
||||||
{
|
{
|
||||||
targetCast.Video3DFormat = sourceCast.Video3DFormat;
|
targetCast.Video3DFormat = sourceCast.Video3DFormat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -15,6 +16,7 @@ using MediaBrowser.Model.Dlna;
|
|||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using TagLib;
|
using TagLib;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.Providers.MediaInfo
|
||||||
@@ -27,6 +29,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<AudioFileProber> _logger;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly LyricResolver _lyricResolver;
|
private readonly LyricResolver _lyricResolver;
|
||||||
private readonly ILyricManager _lyricManager;
|
private readonly ILyricManager _lyricManager;
|
||||||
@@ -34,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||||
@@ -41,6 +45,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
||||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||||
public AudioFileProber(
|
public AudioFileProber(
|
||||||
|
ILogger<AudioFileProber> logger,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
@@ -51,6 +56,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
_logger = logger;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_lyricResolver = lyricResolver;
|
_lyricResolver = lyricResolver;
|
||||||
_lyricManager = lyricManager;
|
_lyricManager = lyricManager;
|
||||||
@@ -146,191 +152,212 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
|
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
|
||||||
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
|
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
|
||||||
{
|
{
|
||||||
using var file = TagLib.File.Create(audio.Path);
|
|
||||||
var tagTypes = file.TagTypesOnDisk;
|
|
||||||
Tag? tags = null;
|
Tag? tags = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var file = TagLib.File.Create(audio.Path);
|
||||||
|
var tagTypes = file.TagTypesOnDisk;
|
||||||
|
|
||||||
if (tagTypes.HasFlag(TagTypes.Id3v2))
|
if (tagTypes.HasFlag(TagTypes.Id3v2))
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Id3v2);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Ape))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Ape);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.FlacMetadata);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Apple))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Apple);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Xiph))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Xiph);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.AudibleMetadata);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Id3v1))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Id3v1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags is not null)
|
|
||||||
{
|
|
||||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
|
||||||
{
|
{
|
||||||
var people = new List<PersonInfo>();
|
tags = file.GetTag(TagTypes.Id3v2);
|
||||||
var albumArtists = tags.AlbumArtists;
|
}
|
||||||
foreach (var albumArtist in albumArtists)
|
else if (tagTypes.HasFlag(TagTypes.Ape))
|
||||||
|
{
|
||||||
|
tags = file.GetTag(TagTypes.Ape);
|
||||||
|
}
|
||||||
|
else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
|
||||||
|
{
|
||||||
|
tags = file.GetTag(TagTypes.FlacMetadata);
|
||||||
|
}
|
||||||
|
else if (tagTypes.HasFlag(TagTypes.Apple))
|
||||||
|
{
|
||||||
|
tags = file.GetTag(TagTypes.Apple);
|
||||||
|
}
|
||||||
|
else if (tagTypes.HasFlag(TagTypes.Xiph))
|
||||||
|
{
|
||||||
|
tags = file.GetTag(TagTypes.Xiph);
|
||||||
|
}
|
||||||
|
else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
|
||||||
|
{
|
||||||
|
tags = file.GetTag(TagTypes.AudibleMetadata);
|
||||||
|
}
|
||||||
|
else if (tagTypes.HasFlag(TagTypes.Id3v1))
|
||||||
|
{
|
||||||
|
tags = file.GetTag(TagTypes.Id3v1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "TagLib-Sharp does not support this audio");
|
||||||
|
}
|
||||||
|
|
||||||
|
tags ??= new TagLib.Id3v2.Tag();
|
||||||
|
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
|
||||||
|
tags.Album ??= mediaInfo.Album;
|
||||||
|
tags.Title ??= mediaInfo.Name;
|
||||||
|
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
||||||
|
tags.Performers ??= mediaInfo.Artists;
|
||||||
|
tags.Genres ??= mediaInfo.Genres;
|
||||||
|
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
|
||||||
|
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
|
||||||
|
|
||||||
|
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||||
|
{
|
||||||
|
var people = new List<PersonInfo>();
|
||||||
|
var albumArtists = tags.AlbumArtists;
|
||||||
|
foreach (var albumArtist in albumArtists)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(albumArtist))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(albumArtist))
|
PeopleHelper.AddPerson(people, new PersonInfo
|
||||||
{
|
{
|
||||||
PeopleHelper.AddPerson(people, new PersonInfo
|
Name = albumArtist,
|
||||||
{
|
Type = PersonKind.AlbumArtist
|
||||||
Name = albumArtist,
|
});
|
||||||
Type = PersonKind.AlbumArtist
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var performers = tags.Performers;
|
var performers = tags.Performers;
|
||||||
foreach (var performer in performers)
|
foreach (var performer in performers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(performer))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(performer))
|
PeopleHelper.AddPerson(people, new PersonInfo
|
||||||
{
|
{
|
||||||
PeopleHelper.AddPerson(people, new PersonInfo
|
Name = performer,
|
||||||
{
|
Type = PersonKind.Artist
|
||||||
Name = performer,
|
});
|
||||||
Type = PersonKind.Artist
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var composer in tags.Composers)
|
foreach (var composer in tags.Composers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(composer))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(composer))
|
PeopleHelper.AddPerson(people, new PersonInfo
|
||||||
{
|
{
|
||||||
PeopleHelper.AddPerson(people, new PersonInfo
|
Name = composer,
|
||||||
{
|
Type = PersonKind.Composer
|
||||||
Name = composer,
|
});
|
||||||
Type = PersonKind.Composer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_libraryManager.UpdatePeople(audio, people);
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata && performers.Length != 0)
|
|
||||||
{
|
|
||||||
audio.Artists = performers;
|
|
||||||
}
|
|
||||||
else if (!options.ReplaceAllMetadata
|
|
||||||
&& (audio.Artists is null || audio.Artists.Count == 0))
|
|
||||||
{
|
|
||||||
audio.Artists = performers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (albumArtists.Length == 0)
|
|
||||||
{
|
|
||||||
// Album artists not provided, fall back to performers (artists).
|
|
||||||
albumArtists = performers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata && albumArtists.Length != 0)
|
|
||||||
{
|
|
||||||
audio.AlbumArtists = albumArtists;
|
|
||||||
}
|
|
||||||
else if (!options.ReplaceAllMetadata
|
|
||||||
&& (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
|
|
||||||
{
|
|
||||||
audio.AlbumArtists = albumArtists;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
|
_libraryManager.UpdatePeople(audio, people);
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata && performers.Length != 0)
|
||||||
{
|
{
|
||||||
audio.Name = tags.Title;
|
audio.Artists = performers;
|
||||||
|
}
|
||||||
|
else if (!options.ReplaceAllMetadata
|
||||||
|
&& (audio.Artists is null || audio.Artists.Count == 0))
|
||||||
|
{
|
||||||
|
audio.Artists = performers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata)
|
if (albumArtists.Length == 0)
|
||||||
{
|
{
|
||||||
audio.Album = tags.Album;
|
// Album artists not provided, fall back to performers (artists).
|
||||||
audio.IndexNumber = Convert.ToInt32(tags.Track);
|
albumArtists = performers;
|
||||||
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
audio.Album ??= tags.Album;
|
|
||||||
audio.IndexNumber ??= Convert.ToInt32(tags.Track);
|
|
||||||
audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.Year != 0)
|
if (options.ReplaceAllMetadata && albumArtists.Length != 0)
|
||||||
{
|
{
|
||||||
var year = Convert.ToInt32(tags.Year);
|
audio.AlbumArtists = albumArtists;
|
||||||
audio.ProductionYear = year;
|
}
|
||||||
|
else if (!options.ReplaceAllMetadata
|
||||||
|
&& (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
|
||||||
|
{
|
||||||
|
audio.AlbumArtists = albumArtists;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!audio.PremiereDate.HasValue)
|
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
|
||||||
|
{
|
||||||
|
audio.Name = tags.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata)
|
||||||
|
{
|
||||||
|
audio.Album = tags.Album;
|
||||||
|
audio.IndexNumber = Convert.ToInt32(tags.Track);
|
||||||
|
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
audio.Album ??= tags.Album;
|
||||||
|
audio.IndexNumber ??= Convert.ToInt32(tags.Track);
|
||||||
|
audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags.Year != 0)
|
||||||
|
{
|
||||||
|
var year = Convert.ToInt32(tags.Year);
|
||||||
|
audio.ProductionYear = year;
|
||||||
|
|
||||||
|
if (!audio.PremiereDate.HasValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
audio.PremiereDate = new DateTime(year, 01, 01);
|
audio.PremiereDate = new DateTime(year, 01, 01);
|
||||||
}
|
}
|
||||||
}
|
catch (ArgumentOutOfRangeException ex)
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
|
||||||
{
|
|
||||||
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
|
|
||||||
? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
|
|
||||||
: audio.Genres;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!double.IsNaN(tags.ReplayGainTrackGain))
|
|
||||||
{
|
|
||||||
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
|
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
|
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
|
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
|
|
||||||
{
|
|
||||||
// Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
|
|
||||||
// See https://github.com/mono/taglib-sharp/issues/304
|
|
||||||
var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
|
|
||||||
if (trackMbId is not null)
|
|
||||||
{
|
{
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
|
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save extracted lyrics if they exist,
|
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||||
// and if the audio doesn't yet have lyrics.
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(tags.Lyrics)
|
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
|
||||||
&& tryExtractEmbeddedLyrics)
|
? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
|
||||||
|
: audio.Genres;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!double.IsNaN(tags.ReplayGainTrackGain))
|
||||||
|
{
|
||||||
|
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
||||||
|
{
|
||||||
|
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
|
||||||
|
{
|
||||||
|
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
|
||||||
|
{
|
||||||
|
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
|
||||||
|
{
|
||||||
|
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
|
||||||
|
{
|
||||||
|
// Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
|
||||||
|
// See https://github.com/mono/taglib-sharp/issues/304
|
||||||
|
var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
|
||||||
|
if (trackMbId is not null)
|
||||||
{
|
{
|
||||||
await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
|
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save extracted lyrics if they exist,
|
||||||
|
// and if the audio doesn't yet have lyrics.
|
||||||
|
if (!string.IsNullOrWhiteSpace(tags.Lyrics)
|
||||||
|
&& tryExtractEmbeddedLyrics)
|
||||||
|
{
|
||||||
|
await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddExternalLyrics(
|
private void AddExternalLyrics(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -103,6 +104,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_subtitleResolver);
|
_subtitleResolver);
|
||||||
|
|
||||||
_audioProber = new AudioFileProber(
|
_audioProber = new AudioFileProber(
|
||||||
|
loggerFactory.CreateLogger<AudioFileProber>(),
|
||||||
mediaSourceManager,
|
mediaSourceManager,
|
||||||
mediaEncoder,
|
mediaEncoder,
|
||||||
itemRepo,
|
itemRepo,
|
||||||
@@ -140,19 +142,15 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
&& item.SupportsLocalMetadata
|
&& item.SupportsLocalMetadata
|
||||||
&& !video.IsPlaceHolder)
|
&& !video.IsPlaceHolder)
|
||||||
{
|
{
|
||||||
if (!video.SubtitleFiles.SequenceEqual(
|
var externalFiles = new HashSet<string>(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
_subtitleResolver.GetExternalFiles(video, directoryService, false)
|
if (!new HashSet<string>(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!video.AudioFiles.SequenceEqual(
|
externalFiles = new HashSet<string>(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
_audioResolver.GetExternalFiles(video, directoryService, false)
|
if (!new HashSet<string>(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
||||||
return true;
|
return true;
|
||||||
@@ -160,14 +158,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item is Audio audio
|
if (item is Audio audio
|
||||||
&& item.SupportsLocalMetadata
|
&& item.SupportsLocalMetadata)
|
||||||
&& !audio.LyricFiles.SequenceEqual(
|
|
||||||
_lyricResolver.GetExternalFiles(audio, directoryService, false)
|
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
|
var externalFiles = new HashSet<string>(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
return true;
|
if (!new HashSet<string>(audio.LyricFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -23,22 +23,6 @@ namespace MediaBrowser.Providers.Movies
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override bool IsFullLocalMetadata(Movie item)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(item.Overview))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.ProductionYear.HasValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.IsFullLocalMetadata(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
|
protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -23,22 +24,6 @@ namespace MediaBrowser.Providers.Movies
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override bool IsFullLocalMetadata(Trailer item)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(item.Overview))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.ProductionYear.HasValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.IsFullLocalMetadata(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
|
protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
|
||||||
{
|
{
|
||||||
@@ -48,6 +33,10 @@ namespace MediaBrowser.Providers.Movies
|
|||||||
{
|
{
|
||||||
target.Item.TrailerTypes = source.Item.TrailerTypes;
|
target.Item.TrailerTypes = source.Item.TrailerTypes;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,10 @@ namespace MediaBrowser.Providers.Music
|
|||||||
{
|
{
|
||||||
targetItem.Artists = sourceItem.Artists;
|
targetItem.Artists = sourceItem.Artists;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
|
if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -60,6 +61,10 @@ namespace MediaBrowser.Providers.Music
|
|||||||
{
|
{
|
||||||
targetItem.Artists = sourceItem.Artists;
|
targetItem.Artists = sourceItem.Artists;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(targetItem.Album))
|
if (replaceData || string.IsNullOrEmpty(targetItem.Album))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -45,6 +46,10 @@ namespace MediaBrowser.Providers.Music
|
|||||||
{
|
{
|
||||||
targetItem.Artists = sourceItem.Artists;
|
targetItem.Artists = sourceItem.Artists;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PlaylistsNET.Content;
|
using PlaylistsNET.Content;
|
||||||
|
|
||||||
@@ -24,11 +26,16 @@ namespace MediaBrowser.Providers.Playlists
|
|||||||
IPreRefreshProvider,
|
IPreRefreshProvider,
|
||||||
IHasItemChangeMonitor
|
IHasItemChangeMonitor
|
||||||
{
|
{
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ILogger<PlaylistItemsProvider> _logger;
|
private readonly ILogger<PlaylistItemsProvider> _logger;
|
||||||
|
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
||||||
|
|
||||||
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger)
|
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Playlist Reader";
|
public string Name => "Playlist Reader";
|
||||||
@@ -54,114 +61,122 @@ namespace MediaBrowser.Providers.Playlists
|
|||||||
|
|
||||||
item.LinkedChildren = items;
|
item.LinkedChildren = items;
|
||||||
|
|
||||||
return Task.FromResult(ItemUpdateType.None);
|
return Task.FromResult(ItemUpdateType.MetadataImport);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
||||||
{
|
{
|
||||||
|
var libraryRoots = _libraryManager.GetUserRootFolder().Children
|
||||||
|
.OfType<CollectionFolder>()
|
||||||
|
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
|
||||||
|
.SelectMany(f => f.PhysicalLocations)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
using (var stream = File.OpenRead(path))
|
using (var stream = File.OpenRead(path))
|
||||||
{
|
{
|
||||||
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetWplItems(stream, path);
|
return GetWplItems(stream, path, libraryRoots);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetZplItems(stream, path);
|
return GetZplItems(stream, path, libraryRoots);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetM3uItems(stream, path);
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetM3u8Items(stream, path);
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetPlsItems(stream, path);
|
return GetPlsItems(stream, path, libraryRoots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Enumerable.Empty<LinkedChild>();
|
return Enumerable.Empty<LinkedChild>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string path)
|
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
{
|
{
|
||||||
var content = new PlsContent();
|
var content = new PlsContent();
|
||||||
var playlist = content.GetFromStream(stream);
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
return playlist.PlaylistEntries
|
||||||
{
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
.Where(i => i is not null);
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetM3u8Items(Stream stream, string path)
|
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
{
|
{
|
||||||
var content = new M3uContent();
|
var content = new M3uContent();
|
||||||
var playlist = content.GetFromStream(stream);
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
return playlist.PlaylistEntries
|
||||||
{
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
.Where(i => i is not null);
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string path)
|
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
{
|
|
||||||
var content = new M3uContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
|
||||||
{
|
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string path)
|
|
||||||
{
|
{
|
||||||
var content = new ZplContent();
|
var content = new ZplContent();
|
||||||
var playlist = content.GetFromStream(stream);
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
return playlist.PlaylistEntries
|
||||||
{
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
.Where(i => i is not null);
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string path)
|
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
{
|
{
|
||||||
var content = new WplContent();
|
var content = new WplContent();
|
||||||
var playlist = content.GetFromStream(stream);
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
return playlist.PlaylistEntries
|
||||||
{
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
.Where(i => i is not null);
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetPlaylistItemPath(string itemPath, string containingPlaylistFolder)
|
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
|
||||||
{
|
{
|
||||||
if (!File.Exists(itemPath))
|
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
|
||||||
{
|
{
|
||||||
var path = Path.Combine(Path.GetDirectoryName(containingPlaylistFolder), itemPath);
|
return new LinkedChild
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
{
|
||||||
return path;
|
Path = parsedPath,
|
||||||
|
Type = LinkedChildType.Manual
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
|
||||||
|
{
|
||||||
|
path = null;
|
||||||
|
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
|
||||||
|
if (!File.Exists(pathToCheck))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var libraryPath in libraryPaths)
|
||||||
|
{
|
||||||
|
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
path = pathToCheck;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return itemPath;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -49,8 +50,24 @@ namespace MediaBrowser.Providers.Playlists
|
|||||||
if (mergeMetadataSettings)
|
if (mergeMetadataSettings)
|
||||||
{
|
{
|
||||||
targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
|
targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
|
||||||
targetItem.LinkedChildren = sourceItem.LinkedChildren;
|
|
||||||
targetItem.Shares = sourceItem.Shares;
|
if (replaceData || targetItem.LinkedChildren.Length == 0)
|
||||||
|
{
|
||||||
|
targetItem.LinkedChildren = sourceItem.LinkedChildren;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceData || targetItem.Shares.Count == 0)
|
||||||
|
{
|
||||||
|
targetItem.Shares = sourceItem.Shares;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,17 +278,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
|
|
||||||
series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
|
series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
|
||||||
|
|
||||||
if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase)
|
if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus))
|
||||||
|| string.Equals(seriesResult.Status, "Canceled", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
series.Status = SeriesStatus.Ended;
|
series.Status = seriesStatus;
|
||||||
series.EndDate = seriesResult.LastAirDate;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
series.Status = SeriesStatus.Continuing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
series.EndDate = seriesResult.LastAirDate;
|
||||||
series.PremiereDate = seriesResult.FirstAirDate;
|
series.PremiereDate = seriesResult.FirstAirDate;
|
||||||
|
|
||||||
var ids = seriesResult.ExternalIds;
|
var ids = seriesResult.ExternalIds;
|
||||||
|
|||||||
@@ -62,23 +62,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
RemoveObsoleteEpisodes(item);
|
RemoveObsoleteEpisodes(item);
|
||||||
RemoveObsoleteSeasons(item);
|
RemoveObsoleteSeasons(item);
|
||||||
await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override bool IsFullLocalMetadata(Series item)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(item.Overview))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.ProductionYear.HasValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.IsFullLocalMetadata(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -88,20 +72,6 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
var sourceItem = source.Item;
|
var sourceItem = source.Item;
|
||||||
var targetItem = target.Item;
|
var targetItem = target.Item;
|
||||||
var sourceSeasonNames = sourceItem.SeasonNames;
|
|
||||||
var targetSeasonNames = targetItem.SeasonNames;
|
|
||||||
|
|
||||||
if (replaceData || targetSeasonNames.Count == 0)
|
|
||||||
{
|
|
||||||
targetItem.SeasonNames = sourceSeasonNames;
|
|
||||||
}
|
|
||||||
else if (targetSeasonNames.Count != sourceSeasonNames.Count || !sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey))
|
|
||||||
{
|
|
||||||
foreach (var (number, name) in sourceSeasonNames)
|
|
||||||
{
|
|
||||||
targetSeasonNames.TryAdd(number, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
|
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
|
||||||
{
|
{
|
||||||
@@ -158,7 +128,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteEpisodes(Series series)
|
private void RemoveObsoleteEpisodes(Series series)
|
||||||
{
|
{
|
||||||
var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList();
|
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
|
||||||
var numberOfEpisodes = episodes.Count;
|
var numberOfEpisodes = episodes.Count;
|
||||||
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
||||||
for (var i = 0; i < numberOfEpisodes; i++)
|
for (var i = 0; i < numberOfEpisodes; i++)
|
||||||
@@ -214,14 +184,12 @@ namespace MediaBrowser.Providers.TV
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates seasons for all episodes if they don't exist.
|
/// Creates seasons for all episodes if they don't exist.
|
||||||
/// If no season number can be determined, a dummy season will be created.
|
/// If no season number can be determined, a dummy season will be created.
|
||||||
/// Updates seasons names.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="series">The series.</param>
|
/// <param name="series">The series.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The async task.</returns>
|
/// <returns>The async task.</returns>
|
||||||
private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken)
|
private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var seasonNames = series.SeasonNames;
|
|
||||||
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
|
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
|
||||||
var seasons = seriesChildren.OfType<Season>().ToList();
|
var seasons = seriesChildren.OfType<Season>().ToList();
|
||||||
var uniqueSeasonNumbers = seriesChildren
|
var uniqueSeasonNumbers = seriesChildren
|
||||||
@@ -233,23 +201,12 @@ namespace MediaBrowser.Providers.TV
|
|||||||
foreach (var seasonNumber in uniqueSeasonNumbers)
|
foreach (var seasonNumber in uniqueSeasonNumbers)
|
||||||
{
|
{
|
||||||
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
||||||
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
if (!seasons.Any(i => i.IndexNumber == seasonNumber))
|
||||||
|
|
||||||
if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName))
|
|
||||||
{
|
|
||||||
seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSeason is null)
|
|
||||||
{
|
{
|
||||||
|
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
||||||
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||||
series.AddChild(season);
|
series.AddChild(season);
|
||||||
}
|
}
|
||||||
else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
existingSeason.Name = seasonName;
|
|
||||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
|
|||||||
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
||||||
bool replace = options.ReplaceAllImages;
|
bool replace = options.ReplaceAllImages;
|
||||||
|
|
||||||
if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
|
if (!enableDuringScan.GetValueOrDefault(false))
|
||||||
{
|
{
|
||||||
return ItemUpdateType.None;
|
return ItemUpdateType.None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Emby.Naming.TV;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Extensions;
|
using MediaBrowser.Controller.Extensions;
|
||||||
@@ -87,7 +87,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(status))
|
if (!string.IsNullOrWhiteSpace(status))
|
||||||
{
|
{
|
||||||
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
|
if (TvParserHelpers.TryParseSeriesStatus(status, out var seriesStatus))
|
||||||
{
|
{
|
||||||
item.Status = seriesStatus;
|
item.Status = seriesStatus;
|
||||||
}
|
}
|
||||||
@@ -100,19 +100,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Season names are processed by SeriesNfoSeasonParser
|
||||||
case "namedseason":
|
case "namedseason":
|
||||||
{
|
reader.Skip();
|
||||||
var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
|
break;
|
||||||
var name = reader.ReadElementContentAsString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(name) && parsed)
|
|
||||||
{
|
|
||||||
item.SeasonNames[seasonNumber] = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
base.FetchDataFromXmlNode(reader, itemResult);
|
base.FetchDataFromXmlNode(reader, itemResult);
|
||||||
break;
|
break;
|
||||||
|
|||||||
60
MediaBrowser.XbmcMetadata/Parsers/SeriesNfoSeasonParser.cs
Normal file
60
MediaBrowser.XbmcMetadata/Parsers/SeriesNfoSeasonParser.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Xml;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaBrowser.XbmcMetadata.Parsers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// NFO parser for seasons based on series NFO.
|
||||||
|
/// </summary>
|
||||||
|
public class SeriesNfoSeasonParser : BaseNfoParser<Season>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SeriesNfoSeasonParser"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
|
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||||
|
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
|
||||||
|
public SeriesNfoSeasonParser(
|
||||||
|
ILogger logger,
|
||||||
|
IConfigurationManager config,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IUserDataManager userDataManager,
|
||||||
|
IDirectoryService directoryService)
|
||||||
|
: base(logger, config, providerManager, userManager, userDataManager, directoryService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool SupportsUrlAfterClosingXmlTag => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Season> itemResult)
|
||||||
|
{
|
||||||
|
var item = itemResult.Item;
|
||||||
|
|
||||||
|
if (reader.Name == "namedseason")
|
||||||
|
{
|
||||||
|
var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
|
||||||
|
var name = reader.ReadElementContentAsString();
|
||||||
|
|
||||||
|
if (parsed && !string.IsNullOrWhiteSpace(name) && item.IndexNumber.HasValue && seasonNumber == item.IndexNumber.Value)
|
||||||
|
{
|
||||||
|
item.Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reader.Skip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,10 @@ namespace MediaBrowser.XbmcMetadata.Providers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result.Item = new T();
|
result.Item = new T
|
||||||
|
{
|
||||||
|
IndexNumber = info.IndexNumber
|
||||||
|
};
|
||||||
|
|
||||||
Fetch(result, path, cancellationToken);
|
Fetch(result, path, cancellationToken);
|
||||||
result.HasMetadata = true;
|
result.HasMetadata = true;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.XbmcMetadata.Parsers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaBrowser.XbmcMetadata.Providers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// NFO provider for seasons based on series NFO.
|
||||||
|
/// </summary>
|
||||||
|
public class SeriesNfoSeasonProvider : BaseNfoProvider<Season>
|
||||||
|
{
|
||||||
|
private readonly ILogger<SeriesNfoSeasonProvider> _logger;
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IUserDataManager _userDataManager;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SeriesNfoSeasonProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{SeasonFromSeriesNfoProvider}"/> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||||
|
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
public SeriesNfoSeasonProvider(
|
||||||
|
ILogger<SeriesNfoSeasonProvider> logger,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IConfigurationManager config,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IUserDataManager userDataManager,
|
||||||
|
IDirectoryService directoryService,
|
||||||
|
ILibraryManager libraryManager)
|
||||||
|
: base(fileSystem)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_config = config;
|
||||||
|
_providerManager = providerManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_userDataManager = userDataManager;
|
||||||
|
_directoryService = directoryService;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
new SeriesNfoSeasonParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
|
||||||
|
{
|
||||||
|
var seasonPath = info.Path;
|
||||||
|
if (seasonPath is not null)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(seasonPath, "tvshow.nfo");
|
||||||
|
if (Path.Exists(path))
|
||||||
|
{
|
||||||
|
return directoryService.GetFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var seriesPath = _libraryManager.GetItemById(info.ParentId)?.Path;
|
||||||
|
if (seriesPath is not null)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(seriesPath, "tvshow.nfo");
|
||||||
|
if (Path.Exists(path))
|
||||||
|
{
|
||||||
|
return directoryService.GetFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
README.md
14
README.md
@@ -153,20 +153,20 @@ API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index
|
|||||||
|
|
||||||
As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
|
As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
|
||||||
|
|
||||||
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 secounds to load all extensions and prepare the enviorment while vscode is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
|
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 seconds to load all extensions and prepare the environment while VS Code is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
|
||||||
|
|
||||||
**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public.
|
**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VS Code window to public.
|
||||||
|
|
||||||
**NOTE:** When first opening the server instance with any WebUI, you will be send to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
|
**NOTE:** When first opening the server instance with any WebUI, you will be sent to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
|
||||||
|
|
||||||
There are two configurations for you to chose from.
|
There are two configurations for you to choose from.
|
||||||
#### Default - Development Jellyfin Server
|
#### Default - Development Jellyfin Server
|
||||||
This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run though the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` lunch config to start the server.
|
This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run through the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` launch config to start the server.
|
||||||
|
|
||||||
> Keep in mind that as this has no web client you have to connect to it via an extenal client. This can be just another codespace container running the WebUI. vuejs does not work from the getgo as it does not support the setup steps.
|
> Keep in mind that as this has no web client you have to connect to it via an external client. This can be just another codespace container running the WebUI. vuejs does not work from the get-go as it does not support the setup steps.
|
||||||
|
|
||||||
#### Development Jellyfin Server ffmpeg
|
#### Development Jellyfin Server ffmpeg
|
||||||
this extens the default server with an default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
|
this extends the default server with a default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
|
||||||
If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file.
|
If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file.
|
||||||
|
|
||||||
Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled.
|
Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.9.1")]
|
[assembly: AssemblyVersion("10.10.0")]
|
||||||
[assembly: AssemblyFileVersion("10.9.1")]
|
[assembly: AssemblyFileVersion("10.10.0")]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user