mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-13 04:23:01 +03:00
Compare commits
235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
478d8b07bf | ||
|
|
c9b6ebd94f | ||
|
|
30fc089dd5 | ||
|
|
25f02658f0 | ||
|
|
2266a00337 | ||
|
|
afeff31dca | ||
|
|
476dc01f4d | ||
|
|
b81b674ae1 | ||
|
|
15eb7a25b9 | ||
|
|
aadd57bc48 | ||
|
|
cbbe5db813 | ||
|
|
4601097d3e | ||
|
|
10cd9a7f79 | ||
|
|
6cf98d4930 | ||
|
|
34a65980e3 | ||
|
|
6010bc01c3 | ||
|
|
a00f9e1a10 | ||
|
|
85078d8f10 | ||
|
|
1606b6c0f6 | ||
|
|
f097aad01e | ||
|
|
bf53f1ae38 | ||
|
|
31237f778a | ||
|
|
55d245a77b | ||
|
|
9f35f56eaf | ||
|
|
f2a5ccf102 | ||
|
|
2b78980747 | ||
|
|
a89678074e | ||
|
|
d813f83b4a | ||
|
|
37b7e953f7 | ||
|
|
08b64c5502 | ||
|
|
23a660e917 | ||
|
|
78eb9b2f78 | ||
|
|
d90f504ca7 | ||
|
|
56104d3042 | ||
|
|
fcec1fcc4d | ||
|
|
1a14902da8 | ||
|
|
34bdf8bf78 | ||
|
|
7ff3f6af6c | ||
|
|
bd8b0c4c03 | ||
|
|
0c560a313a | ||
|
|
8882bb495c | ||
|
|
e4078f984a | ||
|
|
8e5a2f565c | ||
|
|
8b442a7749 | ||
|
|
b63f7a2bc0 | ||
|
|
f9e7d5229e | ||
|
|
b24d05bff7 | ||
|
|
fd009fc71b | ||
|
|
302eea1cb7 | ||
|
|
b116a2742e | ||
|
|
99a04e23d9 | ||
|
|
19a89d5a60 | ||
|
|
feb20c131a | ||
|
|
ec82023265 | ||
|
|
e4f3f0b3b6 | ||
|
|
28274d4c75 | ||
|
|
b6595e4efc | ||
|
|
4046ef1c13 | ||
|
|
b25d6d1e48 | ||
|
|
cf59140276 | ||
|
|
cc4563a477 | ||
|
|
0d984b5162 | ||
|
|
279cba008b | ||
|
|
2b5d458456 | ||
|
|
f41efb3b2c | ||
|
|
0155293c64 | ||
|
|
b78efd6b1e | ||
|
|
bfcc09db8a | ||
|
|
a46c17e19f | ||
|
|
b0bb22b650 | ||
|
|
0c039145e5 | ||
|
|
2a3c904a9f | ||
|
|
7cbdb6708b | ||
|
|
7058db2b04 | ||
|
|
8f7df590cd | ||
|
|
0f67a5ba2f | ||
|
|
19fb00b5b7 | ||
|
|
8683253c6d | ||
|
|
918a36d564 | ||
|
|
57ae0b5796 | ||
|
|
262f7dd98f | ||
|
|
4714b3af67 | ||
|
|
0359035000 | ||
|
|
b14edb8876 | ||
|
|
47c5e0c2c7 | ||
|
|
8709d94783 | ||
|
|
23b1251393 | ||
|
|
484aea1cdb | ||
|
|
d1c00ba4ed | ||
|
|
b1a5fe2f55 | ||
|
|
d7ff6d023c | ||
|
|
253e95dcba | ||
|
|
c7ce1aa4c7 | ||
|
|
3d87885577 | ||
|
|
a7e2271845 | ||
|
|
2a02abee46 | ||
|
|
ab4315742f | ||
|
|
2ddb15c784 | ||
|
|
95c7d997c1 | ||
|
|
e2c909f50f | ||
|
|
a53ea029fa | ||
|
|
869dab2ba2 | ||
|
|
d2be2ee480 | ||
|
|
bc8ef94f0d | ||
|
|
ed1b880359 | ||
|
|
7221e7ca68 | ||
|
|
0392daa103 | ||
|
|
d602b6dbc5 | ||
|
|
b8a0cf6a9e | ||
|
|
26419c64f5 | ||
|
|
a71e2d9f0a | ||
|
|
cfe67ff17d | ||
|
|
78e3ee15f9 | ||
|
|
2cb74e3dd0 | ||
|
|
8e979bdb4b | ||
|
|
e099fd6141 | ||
|
|
35962bcc42 | ||
|
|
ae584beaac | ||
|
|
563033786f | ||
|
|
f8c7f36a34 | ||
|
|
4746c88633 | ||
|
|
b7d6bedbbb | ||
|
|
8db79c05dd | ||
|
|
8fa7ff647a | ||
|
|
d0336cd67e | ||
|
|
cfab4eb2fc | ||
|
|
5f1c5009d3 | ||
|
|
97d7151289 | ||
|
|
475fa36ea3 | ||
|
|
e8d1ee0934 | ||
|
|
d07ec4ad0f | ||
|
|
684dfedbcc | ||
|
|
730b01fb14 | ||
|
|
45e8872cc0 | ||
|
|
cc2c00d764 | ||
|
|
402a5e2c9f | ||
|
|
bcf884ccfa | ||
|
|
2eece01acc | ||
|
|
ef985896e2 | ||
|
|
5e7514243c | ||
|
|
b9c0fc69e8 | ||
|
|
4a344bebc0 | ||
|
|
b2d54b82fa | ||
|
|
e7b1162cb3 | ||
|
|
d89e5a0074 | ||
|
|
4a54e5ddeb | ||
|
|
d9232e05f1 | ||
|
|
52be8be28f | ||
|
|
ab6c2424db | ||
|
|
eb437e7163 | ||
|
|
2ddf2a7866 | ||
|
|
60232ce9be | ||
|
|
952995f796 | ||
|
|
933a285bf5 | ||
|
|
f8da69f8e5 | ||
|
|
d5e29bfce3 | ||
|
|
ab36c4c011 | ||
|
|
c6e29647fc | ||
|
|
95a6291c34 | ||
|
|
fa2bff30f6 | ||
|
|
58041e1f9d | ||
|
|
447f73caf4 | ||
|
|
9145be6bfc | ||
|
|
86129589ef | ||
|
|
f5a8fca22f | ||
|
|
2a612611b8 | ||
|
|
0b64426cf2 | ||
|
|
f3bf9bcdc8 | ||
|
|
06a5ddda5e | ||
|
|
8a5a93ee80 | ||
|
|
6777f47e0e | ||
|
|
cf04e1d8e5 | ||
|
|
d608f1e3cc | ||
|
|
86f5c93434 | ||
|
|
4fcbeef5e6 | ||
|
|
7d983ae0dd | ||
|
|
a2ab34ef4c | ||
|
|
77abafca8e | ||
|
|
e67eb48540 | ||
|
|
37d7e8f5bf | ||
|
|
e6eef8bece | ||
|
|
52cfd9f261 | ||
|
|
9a9e8e2648 | ||
|
|
2cebd5e05f | ||
|
|
7fa72260ca | ||
|
|
b19b346670 | ||
|
|
99de0ca45f | ||
|
|
c106b399d7 | ||
|
|
c274062e87 | ||
|
|
53de8c0805 | ||
|
|
76854b5eff | ||
|
|
5200633574 | ||
|
|
832e27a8fb | ||
|
|
2da06bc0b1 | ||
|
|
46c748d888 | ||
|
|
430d450828 | ||
|
|
02937873b1 | ||
|
|
d303ca56e3 | ||
|
|
c647143e53 | ||
|
|
dd0ab8ed56 | ||
|
|
18e6c1ef7d | ||
|
|
dec2032e13 | ||
|
|
287e06d6dc | ||
|
|
dc93cc13b5 | ||
|
|
26714e2c62 | ||
|
|
c6c48a2b47 | ||
|
|
f8b67ec44c | ||
|
|
ddd5c302b4 | ||
|
|
9b98638b2b | ||
|
|
2ca8ce6f60 | ||
|
|
56a158e5c9 | ||
|
|
9b65d243a8 | ||
|
|
c45dd5d6fb | ||
|
|
af4b732080 | ||
|
|
1cdf0f5cc4 | ||
|
|
20a1da1855 | ||
|
|
8aee50020b | ||
|
|
c1615419b9 | ||
|
|
15489eeae3 | ||
|
|
80c9589885 | ||
|
|
a5d60c4521 | ||
|
|
2cb052a119 | ||
|
|
0756174b13 | ||
|
|
3f760e6685 | ||
|
|
d5dc4435d9 | ||
|
|
48228430c0 | ||
|
|
f396a95f05 | ||
|
|
717afcdc82 | ||
|
|
25c50bcc5d | ||
|
|
f77a5d0c5c | ||
|
|
6689d837d6 | ||
|
|
c1907354e8 | ||
|
|
efba619acb | ||
|
|
7d271547c6 | ||
|
|
327f92bb2e |
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@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
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@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
|
|||||||
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@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
|
||||||
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@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
||||||
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,15 @@
|
|||||||
<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.HttpOverrides" Version="2.2.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
|
||||||
<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 +42,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" />
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.6.2" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
<PackageVersion Include="xunit" Version="2.7.1" />
|
<PackageVersion Include="xunit" Version="2.8.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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.0</VersionPrefix>
|
<VersionPrefix>10.9.7</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>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
|
|||||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
pathInfo.IsHearingImpaired = true;
|
pathInfo.IsHearingImpaired = true;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -422,7 +422,7 @@ namespace Emby.Server.Implementations
|
|||||||
// Initialize runtime stat collection
|
// Initialize runtime stat collection
|
||||||
if (ConfigurationManager.Configuration.EnableMetrics)
|
if (ConfigurationManager.Configuration.EnableMetrics)
|
||||||
{
|
{
|
||||||
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
_disposableParts.Add(DotNetRuntimeStatsBuilder.Default().StartCollecting());
|
||||||
}
|
}
|
||||||
|
|
||||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|||||||
@@ -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 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
|
|||||||
public abstract class BaseSqliteRepository : IDisposable
|
public abstract class BaseSqliteRepository : IDisposable
|
||||||
{
|
{
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
||||||
|
private SqliteConnection _writeConnection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
||||||
@@ -98,9 +101,55 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SqliteConnection GetConnection()
|
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||||
{
|
{
|
||||||
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
if (!readOnly)
|
||||||
|
{
|
||||||
|
_writeLock.Wait();
|
||||||
|
if (_writeConnection is not null)
|
||||||
|
{
|
||||||
|
return new ManagedConnection(_writeConnection, _writeLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
|
||||||
|
writeConnection.Open();
|
||||||
|
|
||||||
|
if (CacheSize.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JournalSizeLimit.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Synchronous.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PageSize.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||||
|
|
||||||
|
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
if (CacheSize.HasValue)
|
||||||
@@ -135,17 +184,17 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||||
|
|
||||||
return connection;
|
return new ManagedConnection(connection, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
||||||
{
|
{
|
||||||
var command = connection.CreateCommand();
|
var command = connection.CreateCommand();
|
||||||
command.CommandText = sql;
|
command.CommandText = sql;
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool TableExists(SqliteConnection connection, string name)
|
protected bool TableExists(ManagedConnection connection, string name)
|
||||||
{
|
{
|
||||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
@@ -159,7 +208,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
||||||
{
|
{
|
||||||
var columnNames = new List<string>();
|
var columnNames = new List<string>();
|
||||||
|
|
||||||
@@ -174,7 +223,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return columnNames;
|
return columnNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||||
{
|
{
|
||||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -207,6 +256,24 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dispose)
|
||||||
|
{
|
||||||
|
_writeLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_writeConnection.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_writeLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeLock.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeConnection = null;
|
||||||
|
_writeLock = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Data;
|
||||||
|
|
||||||
|
public sealed class ManagedConnection : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim? _writeLock;
|
||||||
|
|
||||||
|
private SqliteConnection _db;
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_writeLock = writeLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SqliteTransaction BeginTransaction()
|
||||||
|
=> _db.BeginTransaction();
|
||||||
|
|
||||||
|
public SqliteCommand CreateCommand()
|
||||||
|
=> _db.CreateCommand();
|
||||||
|
|
||||||
|
public void Execute(string commandText)
|
||||||
|
=> _db.Execute(commandText);
|
||||||
|
|
||||||
|
public SqliteCommand PrepareStatement(string sql)
|
||||||
|
=> _db.PrepareStatement(sql);
|
||||||
|
|
||||||
|
public IEnumerable<SqliteDataReader> Query(string commandText)
|
||||||
|
=> _db.Query(commandText);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_writeLock is null)
|
||||||
|
{
|
||||||
|
// Read connections are managed with an internal pool
|
||||||
|
_db.Dispose();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Write lock is managed by BaseSqliteRepository
|
||||||
|
// Don't dispose here
|
||||||
|
_writeLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
_db = null!;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
||||||
{
|
{
|
||||||
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
||||||
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
||||||
@@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
||||||
{
|
{
|
||||||
statement.TryBind("@guid", id);
|
statement.TryBind("@guid", id);
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -1888,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var chapters = new List<ChapterInfo>();
|
var chapters = new List<ChapterInfo>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", item.Id);
|
statement.TryBind("@ItemId", item.Id);
|
||||||
@@ -1907,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", item.Id);
|
statement.TryBind("@ItemId", item.Id);
|
||||||
@@ -1981,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
|
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
|
||||||
{
|
{
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
var limit = 100;
|
var limit = 100;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2470,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
|
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -2538,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
var items = new List<BaseItem>();
|
var items = new List<BaseItem>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -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);
|
||||||
@@ -2746,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
var list = new List<BaseItem>();
|
var list = new List<BaseItem>();
|
||||||
var result = new QueryResult<BaseItem>();
|
var result = new QueryResult<BaseItem>();
|
||||||
using var connection = GetConnection();
|
using var connection = GetConnection(true);
|
||||||
using var transaction = connection.BeginTransaction();
|
using var transaction = connection.BeginTransaction();
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
{
|
{
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -2928,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -4477,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
|
private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
|
||||||
{
|
{
|
||||||
using (var statement = PrepareStatement(db, query))
|
using (var statement = PrepareStatement(db, query))
|
||||||
{
|
{
|
||||||
@@ -4510,7 +4509,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||||
{
|
{
|
||||||
// Run this again to bind the params
|
// Run this again to bind the params
|
||||||
@@ -4548,7 +4547,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<PersonInfo>();
|
var list = new List<PersonInfo>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||||
{
|
{
|
||||||
// Run this again to bind the params
|
// Run this again to bind the params
|
||||||
@@ -4633,7 +4632,7 @@ AND Type = @InternalPersonType)");
|
|||||||
return whereClauses;
|
return whereClauses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
|
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
|
||||||
{
|
{
|
||||||
if (itemId.IsEmpty())
|
if (itemId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -4788,7 +4787,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
@@ -4988,8 +4987,8 @@ AND Type = @InternalPersonType)");
|
|||||||
var list = new List<(BaseItem, ItemCounts)>();
|
var list = new List<(BaseItem, ItemCounts)>();
|
||||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var transaction = connection.BeginTransaction(deferred: true))
|
using (var transaction = connection.BeginTransaction())
|
||||||
{
|
{
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
{
|
{
|
||||||
@@ -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,12 +5143,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
||||||
{
|
{
|
||||||
if (itemId.IsEmpty())
|
if (itemId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -5168,7 +5167,7 @@ AND Type = @InternalPersonType)");
|
|||||||
InsertItemValues(itemId, values, db);
|
InsertItemValues(itemId, values, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 100;
|
const int Limit = 100;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -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,24 +5221,25 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
|
private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 100;
|
const int Limit = 100;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5341,7 +5335,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
cmdText += " order by StreamIndex ASC";
|
cmdText += " order by StreamIndex ASC";
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
{
|
{
|
||||||
var list = new List<MediaStream>();
|
var list = new List<MediaStream>();
|
||||||
|
|
||||||
@@ -5394,7 +5388,7 @@ AND Type = @InternalPersonType)");
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
|
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 10;
|
const int Limit = 10;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5728,7 +5722,7 @@ AND Type = @InternalPersonType)");
|
|||||||
cmdText += " order by AttachmentIndex ASC";
|
cmdText += " order by AttachmentIndex ASC";
|
||||||
|
|
||||||
var list = new List<MediaAttachment>();
|
var list = new List<MediaAttachment>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, cmdText))
|
using (var statement = PrepareStatement(connection, cmdText))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", query.ItemId);
|
statement.TryBind("@ItemId", query.ItemId);
|
||||||
@@ -5778,7 +5772,7 @@ AND Type = @InternalPersonType)");
|
|||||||
private void InsertMediaAttachments(
|
private void InsertMediaAttachments(
|
||||||
Guid id,
|
Guid id,
|
||||||
IReadOnlyList<MediaAttachment> attachments,
|
IReadOnlyList<MediaAttachment> attachments,
|
||||||
SqliteConnection db,
|
ManagedConnection db,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const int InsertAtOnce = 10;
|
const int InsertAtOnce = 10;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
|
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
||||||
{
|
{
|
||||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
|
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
||||||
{
|
{
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
|
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
|
||||||
{
|
{
|
||||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||||
{
|
{
|
||||||
@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
{
|
{
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,12 +80,14 @@ namespace Emby.Server.Implementations.IO
|
|||||||
public virtual string MakeAbsolutePath(string folderPath, string filePath)
|
public virtual string MakeAbsolutePath(string folderPath, string filePath)
|
||||||
{
|
{
|
||||||
// path is actually a stream
|
// path is actually a stream
|
||||||
if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal))
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
{
|
{
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
|
var isAbsolutePath = Path.IsPathRooted(filePath) && (!OperatingSystem.IsWindows() || filePath[0] != '\\');
|
||||||
|
|
||||||
|
if (isAbsolutePath)
|
||||||
{
|
{
|
||||||
// absolute local path
|
// absolute local path
|
||||||
return filePath;
|
return filePath;
|
||||||
@@ -97,17 +99,10 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstChar = filePath[0];
|
|
||||||
if (firstChar == '/')
|
|
||||||
{
|
|
||||||
// for this we don't really know
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filePathSpan = filePath.AsSpan();
|
var filePathSpan = filePath.AsSpan();
|
||||||
|
|
||||||
// relative path
|
// relative path on windows
|
||||||
if (firstChar == '\\')
|
if (filePath[0] == '\\')
|
||||||
{
|
{
|
||||||
filePathSpan = filePathSpan.Slice(1);
|
filePathSpan = filePathSpan.Slice(1);
|
||||||
}
|
}
|
||||||
@@ -394,7 +389,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
var info = new FileInfo(path);
|
var info = new FileInfo(path);
|
||||||
|
|
||||||
if (info.Exists &&
|
if (info.Exists &&
|
||||||
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
|
(info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
|
||||||
{
|
{
|
||||||
if (isHidden)
|
if (isHidden)
|
||||||
{
|
{
|
||||||
@@ -422,8 +417,8 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
|
||||||
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
&& (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||||
{
|
{
|
||||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1884,7 +1884,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var index = item.GetImageIndex(img);
|
var index = item.GetImageIndex(img);
|
||||||
image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false);
|
image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
@@ -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 : null
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"DeviceOfflineWithName": "{0} has disconnected",
|
"DeviceOfflineWithName": "{0} has disconnected",
|
||||||
"DeviceOnlineWithName": "{0} is connected",
|
"DeviceOnlineWithName": "{0} is connected",
|
||||||
"External": "External",
|
"External": "External",
|
||||||
"FailedLoginAttemptWithUserName": "Failed login try from {0}",
|
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||||
"Favorites": "Favorites",
|
"Favorites": "Favorites",
|
||||||
"Folders": "Folders",
|
"Folders": "Folders",
|
||||||
"Forced": "Forced",
|
"Forced": "Forced",
|
||||||
|
|||||||
@@ -321,7 +321,11 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
// Try splitting by : to handle "Germany: FSK-18"
|
// Try splitting by : to handle "Germany: FSK-18"
|
||||||
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
|
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
|
var ratingLevelRightPart = rating.AsSpan().RightPart(':');
|
||||||
|
if (ratingLevelRightPart.Length != 0)
|
||||||
|
{
|
||||||
|
return GetRatingLevel(ratingLevelRightPart.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle prefix country code to handle "DE-18"
|
// Handle prefix country code to handle "DE-18"
|
||||||
@@ -332,8 +336,12 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
// Extract culture from country prefix
|
// Extract culture from country prefix
|
||||||
var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
|
var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
|
||||||
|
|
||||||
// Check rating system of culture
|
var ratingLevelRightPart = ratingSpan.RightPart('-');
|
||||||
return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
|
if (ratingLevelRightPart.Length != 0)
|
||||||
|
{
|
||||||
|
// Check rating system of culture
|
||||||
|
return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
Exempt,0
|
Exempt,0
|
||||||
G,0
|
G,0
|
||||||
7+,7
|
7+,7
|
||||||
|
PG,15
|
||||||
M,15
|
M,15
|
||||||
MA,15
|
MA,15
|
||||||
MA15+,15
|
MA15+,15
|
||||||
MA 15+,15
|
MA 15+,15
|
||||||
PG,16
|
|
||||||
16+,16
|
16+,16
|
||||||
R,18
|
R,18
|
||||||
R18+,18
|
R18+,18
|
||||||
|
|||||||
|
@@ -170,8 +170,13 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
private List<Playlist> GetUserPlaylists(Guid userId)
|
private List<Playlist> GetUserPlaylists(Guid userId)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
var playlistsFolder = GetPlaylistsFolder(userId);
|
||||||
|
if (playlistsFolder is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList();
|
return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetTargetPath(string path)
|
private static string GetTargetPath(string path)
|
||||||
@@ -184,11 +189,11 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
|
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
||||||
|
|
||||||
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
|
return Playlist.GetPlaylistItems(items, user, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
||||||
@@ -208,7 +213,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
||||||
|
|
||||||
// Retrieve all the items to be added to the playlist
|
// Retrieve all the items to be added to the playlist
|
||||||
var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options)
|
var newItems = GetPlaylistItems(newItemIds, user, options)
|
||||||
.Where(i => i.SupportsAddingToPlaylist);
|
.Where(i => i.SupportsAddingToPlaylist);
|
||||||
|
|
||||||
// Filter out duplicate items, if necessary
|
// Filter out duplicate items, if necessary
|
||||||
|
|||||||
@@ -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 />
|
||||||
@@ -105,13 +106,20 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
|
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
||||||
|
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), a.Id + ".concat");
|
||||||
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
||||||
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||||
a.LUFS = await CalculateLUFSAsync(
|
try
|
||||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
{
|
||||||
cancellationToken).ConfigureAwait(false);
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
File.Delete(tempFile);
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(tempFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(albums, cancellationToken);
|
_itemRepository.SaveItems(albums, cancellationToken);
|
||||||
@@ -179,16 +187,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
||||||
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
||||||
|
_providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
|
||||||
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
||||||
|
|
||||||
_providerManager.QueueRefresh(
|
|
||||||
folder.Id,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
|
||||||
{
|
|
||||||
ForceSave = true
|
|
||||||
},
|
|
||||||
RefreshPriority.High);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
@@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
DeleteFile(file.FullName);
|
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteEmptyFolders(directory);
|
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteEmptyFolders(string parent)
|
|
||||||
{
|
|
||||||
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(directory);
|
|
||||||
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(directory, false);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteFile(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(path);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
@@ -113,53 +113,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
DeleteFile(file.FullName);
|
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteEmptyFolders(directory);
|
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteEmptyFolders(string parent)
|
|
||||||
{
|
|
||||||
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(directory);
|
|
||||||
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(directory, false);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteFile(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(path);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
string? presentationUniqueKey = null;
|
string? presentationUniqueKey = null;
|
||||||
int? limit = null;
|
int? limit = request.Limit;
|
||||||
if (!request.SeriesId.IsNullOrEmpty())
|
if (!request.SeriesId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
|
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||||||
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|| replaceAllImages
|
|| replaceAllImages
|
||||||
|| replaceAllMetadata,
|
|| replaceAllMetadata,
|
||||||
IsAutomated = false
|
IsAutomated = false,
|
||||||
|
RemoveOldMetadata = replaceAllMetadata
|
||||||
};
|
};
|
||||||
|
|
||||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||||||
// No need to start if scanning the library because it will handle it
|
// No need to start if scanning the library because it will handle it
|
||||||
if (refreshLibrary)
|
if (refreshLibrary)
|
||||||
{
|
{
|
||||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
|
||||||
|
var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (newLib is CollectionFolder folder)
|
||||||
|
{
|
||||||
|
foreach (var child in folder.GetPhysicalFolders())
|
||||||
|
{
|
||||||
|
await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We don't know if this one can be validated individually, trigger a new validation
|
||||||
|
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -319,7 +333,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,25 @@ 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";
|
||||||
|
streamingRequest.AudioCodec = "aac";
|
||||||
|
state.SupportedVideoCodecs = ["h264"];
|
||||||
|
state.Request.VideoCodec = "h264";
|
||||||
|
state.SupportedAudioCodecs = ["aac"];
|
||||||
|
state.Request.AudioCodec = "aac";
|
||||||
|
}
|
||||||
|
|
||||||
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.0</VersionPrefix>
|
<VersionPrefix>10.9.7</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 source {MediaSourceId} not found at {Path} for item {ItemID}", mediaSource.Id, 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>
|
||||||
|
|||||||
106
Jellyfin.Server/Migrations/Routines/FixAudioData.cs
Normal file
106
Jellyfin.Server/Migrations/Routines/FixAudioData.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
|
||||||
|
File.Copy(dbPath, bakPath);
|
||||||
|
_logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, 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 = 5000,
|
||||||
|
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 += results.Count;
|
||||||
|
_logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ namespace Jellyfin.Server
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_restartOnShutdown = false;
|
||||||
_logger.LogCritical(ex, "Error while starting server");
|
_logger.LogCritical(ex, "Error while starting server");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -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.0</VersionPrefix>
|
<VersionPrefix>10.9.7</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;
|
||||||
|
|
||||||
@@ -1952,14 +1949,15 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove it from the item
|
// Remove from file system
|
||||||
RemoveImage(info);
|
|
||||||
|
|
||||||
if (info.IsLocalFile)
|
if (info.IsLocalFile)
|
||||||
{
|
{
|
||||||
FileSystem.DeleteFile(info.Path);
|
FileSystem.DeleteFile(info.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove from item
|
||||||
|
RemoveImage(info);
|
||||||
|
|
||||||
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -364,15 +365,23 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
if (IsFileProtocol)
|
if (IsFileProtocol)
|
||||||
{
|
{
|
||||||
IEnumerable<BaseItem> nonCachedChildren;
|
IEnumerable<BaseItem> nonCachedChildren = [];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nonCachedChildren = GetNonCachedChildren(directoryService);
|
nonCachedChildren = GetNonCachedChildren(directoryService);
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error retrieving children from file system");
|
||||||
|
}
|
||||||
|
catch (SecurityException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error retrieving children from file system");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error retrieving children folder");
|
Logger.LogError(ex, "Error retrieving children");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +603,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||||
var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount;
|
var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
|
||||||
|
|
||||||
var actionBlock = new ActionBlock<int>(
|
var actionBlock = new ActionBlock<int>(
|
||||||
async i =>
|
async i =>
|
||||||
|
|||||||
@@ -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,41 +348,32 @@ 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;
|
|
||||||
|
|
||||||
// add optimization when this setting is not enabled
|
|
||||||
var seriesKey = queryFromSeries ?
|
|
||||||
GetUniqueSeriesKey(this) :
|
|
||||||
GetUniqueSeriesKey(parentSeason);
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
|
AncestorWithPresentationUniqueKey = null,
|
||||||
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
|
SeriesPresentationUniqueKey = GetUniqueSeriesKey(this),
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
MediaBrowser.Controller/IO/FileSystemHelper.cs
Normal file
64
MediaBrowser.Controller/IO/FileSystemHelper.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.IO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper methods for file system management.
|
||||||
|
/// </summary>
|
||||||
|
public static class FileSystemHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileSystem">The fileSystem.</param>
|
||||||
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively delete empty folders.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileSystem">The fileSystem.</param>
|
||||||
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
|
||||||
|
{
|
||||||
|
foreach (var directory in fileSystem.GetDirectoryPaths(path))
|
||||||
|
{
|
||||||
|
DeleteEmptyFolders(fileSystem, directory, logger);
|
||||||
|
if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, false);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,6 +149,14 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
|
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reloads the root media folder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <param name="removeRoot">Is remove the library itself allowed.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
|
||||||
|
|
||||||
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
|
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -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.0</VersionPrefix>
|
<VersionPrefix>10.9.7</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
|
||||||
@@ -1215,8 +1208,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var subtitlePath = state.SubtitleStream.Path;
|
var subtitlePath = state.SubtitleStream.Path;
|
||||||
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||||
|
|
||||||
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
|
||||||
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||||
if (File.Exists(idxFile))
|
if (File.Exists(idxFile))
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
return base.GetChildren(user, true, query);
|
return base.GetChildren(user, true, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (user is not null)
|
if (user is not null)
|
||||||
{
|
{
|
||||||
@@ -177,14 +177,14 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
|
|
||||||
foreach (var item in inputItems)
|
foreach (var item in inputItems)
|
||||||
{
|
{
|
||||||
var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
|
var playlistItems = GetPlaylistItems(item, user, options);
|
||||||
list.AddRange(playlistItems);
|
list.AddRange(playlistItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
|
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (item is MusicGenre musicGenre)
|
if (item is MusicGenre musicGenre)
|
||||||
{
|
{
|
||||||
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
{
|
{
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
IsFolder = false,
|
IsFolder = false,
|
||||||
MediaTypes = [mediaType],
|
MediaTypes = [MediaType.Audio, MediaType.Video],
|
||||||
EnableTotalRecordCount = false,
|
EnableTotalRecordCount = false,
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
|
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<FileSystemMetadata> GetDirectories(string path)
|
||||||
|
{
|
||||||
|
var list = new List<FileSystemMetadata>();
|
||||||
|
var items = GetFileSystemEntries(path);
|
||||||
|
for (var i = 0; i < items.Length; i++)
|
||||||
|
{
|
||||||
|
var item = items[i];
|
||||||
|
if (item.IsDirectory)
|
||||||
|
{
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public List<FileSystemMetadata> GetFiles(string path)
|
public List<FileSystemMetadata> GetFiles(string path)
|
||||||
{
|
{
|
||||||
var list = new List<FileSystemMetadata>();
|
var list = new List<FileSystemMetadata>();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
{
|
{
|
||||||
FileSystemMetadata[] GetFileSystemEntries(string path);
|
FileSystemMetadata[] GetFileSystemEntries(string path);
|
||||||
|
|
||||||
|
List<FileSystemMetadata> GetDirectories(string path);
|
||||||
|
|
||||||
List<FileSystemMetadata> GetFiles(string path);
|
List<FileSystemMetadata> GetFiles(string path);
|
||||||
|
|
||||||
FileSystemMetadata? GetFile(string path);
|
FileSystemMetadata? GetFile(string path);
|
||||||
|
|||||||
@@ -140,6 +140,14 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
||||||
where T : BaseItem;
|
where T : BaseItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the metadata savers for the provided item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item.</param>
|
||||||
|
/// <param name="libraryOptions">The library options.</param>
|
||||||
|
/// <returns>The metadata savers.</returns>
|
||||||
|
IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all metadata plugins.
|
/// Gets all metadata plugins.
|
||||||
/// </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; }
|
||||||
|
|||||||
@@ -38,19 +38,26 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parentPathFiles = directoryService.GetFiles(parentPath);
|
var parentPathFiles = directoryService.GetFiles(parentPath);
|
||||||
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
|
||||||
|
|
||||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
|
var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
|
||||||
|
|
||||||
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
|
var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
|
||||||
|
if (metadataSubDir is not null)
|
||||||
|
{
|
||||||
|
var files = directoryService.GetFiles(metadataSubDir.FullName);
|
||||||
|
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
|
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
|
||||||
{
|
{
|
||||||
|
var list = new List<LocalImageInfo>(1);
|
||||||
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
||||||
|
|
||||||
var list = new List<LocalImageInfo>(1);
|
foreach (var i in filePaths)
|
||||||
|
|
||||||
foreach (var i in parentPathFiles)
|
|
||||||
{
|
{
|
||||||
if (i.IsDirectory)
|
if (i.IsDirectory)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
|
||||||
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
|
return validPlaybackFiles
|
||||||
.Select(f => f.FullName)
|
.Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
|
||||||
.Order()
|
.Where(f => f is not null)
|
||||||
.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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -280,8 +280,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
splitFormat[i] = "mpeg";
|
splitFormat[i] = "mpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle MPEG-2 container
|
// Handle MPEG-TS container
|
||||||
else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(splitFormat[i], "mpegts", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
splitFormat[i] = "ts";
|
splitFormat[i] = "ts";
|
||||||
}
|
}
|
||||||
@@ -624,15 +624,19 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
{
|
{
|
||||||
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "dvbsub";
|
codec = "DVBSUB";
|
||||||
}
|
}
|
||||||
else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(codec, "dvb_teletext", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "PGSSUB";
|
codec = "DVBTXT";
|
||||||
}
|
}
|
||||||
else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(codec, "dvd_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "DVDSUB";
|
codec = "DVDSUB"; // .sub+.idx
|
||||||
|
}
|
||||||
|
else if (string.Equals(codec, "hdmv_pgs_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
codec = "PGSSUB"; // .sup
|
||||||
}
|
}
|
||||||
|
|
||||||
return codec;
|
return codec;
|
||||||
@@ -779,11 +783,10 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (isAudio
|
if (isAudio
|
||||||
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))
|
||||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
stream.Type = MediaStreamType.EmbeddedImage;
|
stream.Type = MediaStreamType.EmbeddedImage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -656,14 +656,14 @@ namespace MediaBrowser.Model.Entities
|
|||||||
{
|
{
|
||||||
string codec = format ?? string.Empty;
|
string codec = format ?? string.Empty;
|
||||||
|
|
||||||
// sub = external .sub file
|
// microdvd and dvdsub/vobsub share the ".sub" file extension, but it's text-based.
|
||||||
|
|
||||||
return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase)
|
|| (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsSubtitleConversionTo(string toCodec)
|
public bool SupportsSubtitleConversionTo(string toCodec)
|
||||||
|
|||||||
@@ -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.0</VersionPrefix>
|
<VersionPrefix>10.9.7</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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Configuration;
|
|||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -100,8 +101,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
saveLocally = false;
|
saveLocally = false;
|
||||||
|
|
||||||
// If season is virtual under a physical series, save locally if using compatible convention
|
// If season is virtual under a physical series, save locally
|
||||||
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
|
if (item is Season season)
|
||||||
{
|
{
|
||||||
var series = season.Series;
|
var series = season.Series;
|
||||||
|
|
||||||
@@ -126,7 +127,11 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
||||||
|
|
||||||
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
string[] retryPaths = [];
|
||||||
|
if (saveLocally)
|
||||||
|
{
|
||||||
|
retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
||||||
|
}
|
||||||
|
|
||||||
// If there are more than one output paths, the stream will need to be seekable
|
// If there are more than one output paths, the stream will need to be seekable
|
||||||
if (paths.Length > 1 && !source.CanSeek)
|
if (paths.Length > 1 && !source.CanSeek)
|
||||||
@@ -183,6 +188,29 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_fileSystem.DeleteFile(currentPath);
|
_fileSystem.DeleteFile(currentPath);
|
||||||
|
|
||||||
|
// Remove local episode metadata directory if it exists and is empty
|
||||||
|
var directory = Path.GetDirectoryName(currentPath);
|
||||||
|
if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var parentDirectoryPath = Directory.GetParent(currentPath).FullName;
|
||||||
|
if (_fileSystem.DirectoryExists(parentDirectoryPath) && !_fileSystem.GetFiles(parentDirectoryPath).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleting empty local metadata folder {Folder}", parentDirectoryPath);
|
||||||
|
Directory.Delete(parentDirectoryPath);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@@ -374,6 +402,47 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
|
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
extension = ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
extension = extension.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (type == ImageType.Primary && saveLocally)
|
||||||
|
{
|
||||||
|
if (season is not null && season.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
var seriesFolder = season.SeriesPath;
|
||||||
|
|
||||||
|
var seasonMarker = season.IndexNumber.Value == 0
|
||||||
|
? "-specials"
|
||||||
|
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var imageFilename = "season" + seasonMarker + "-poster" + extension;
|
||||||
|
|
||||||
|
return Path.Combine(seriesFolder, imageFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == ImageType.Backdrop && saveLocally)
|
||||||
|
{
|
||||||
|
if (season is not null
|
||||||
|
&& season.IndexNumber.HasValue
|
||||||
|
&& (imageIndex is null || imageIndex == 0))
|
||||||
|
{
|
||||||
|
var seriesFolder = season.SeriesPath;
|
||||||
|
|
||||||
|
var seasonMarker = season.IndexNumber.Value == 0
|
||||||
|
? "-specials"
|
||||||
|
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
|
||||||
|
|
||||||
|
return Path.Combine(seriesFolder, imageFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type == ImageType.Thumb && saveLocally)
|
if (type == ImageType.Thumb && saveLocally)
|
||||||
{
|
{
|
||||||
if (season is not null && season.IndexNumber.HasValue)
|
if (season is not null && season.IndexNumber.HasValue)
|
||||||
@@ -447,20 +516,12 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
extension = ".jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
extension = extension.ToLowerInvariant();
|
|
||||||
|
|
||||||
string path = null;
|
string path = null;
|
||||||
|
|
||||||
if (saveLocally)
|
if (saveLocally)
|
||||||
{
|
{
|
||||||
if (type == ImageType.Primary && item is Episode)
|
if (type == ImageType.Primary && item is Episode)
|
||||||
{
|
{
|
||||||
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
|
path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
|
||||||
}
|
}
|
||||||
else if (item.IsInMixedFolder)
|
else if (item.IsInMixedFolder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
||||||
{
|
{
|
||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
IDirectoryService directoryService = refreshOptions?.DirectoryService;
|
var directoryService = refreshOptions?.DirectoryService;
|
||||||
|
|
||||||
if (item is not Photo)
|
if (item is not Photo)
|
||||||
{
|
{
|
||||||
@@ -158,7 +159,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only delete existing multi-images if new ones were added
|
// Only delete existing multi-images if new ones were added
|
||||||
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
|
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
|
||||||
{
|
{
|
||||||
PruneImages(item, oldBackdropImages);
|
PruneImages(item, oldBackdropImages);
|
||||||
@@ -359,10 +360,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < images.Count; i++)
|
foreach (var image in images)
|
||||||
{
|
{
|
||||||
var image = images[i];
|
|
||||||
|
|
||||||
if (image.IsLocalFile)
|
if (image.IsLocalFile)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -371,7 +370,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
// nothing to do, already gone
|
// Nothing to do, already gone
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
@@ -381,6 +380,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.RemoveImages(images);
|
item.RemoveImages(images);
|
||||||
|
|
||||||
|
// Cleanup old metadata directory for episodes if empty
|
||||||
|
if (item is Episode)
|
||||||
|
{
|
||||||
|
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
||||||
|
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(oldLocalMetadataDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -413,12 +422,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
var changed = item.ValidateImages();
|
var changed = item.ValidateImages();
|
||||||
var foundImageTypes = new List<ImageType>();
|
var foundImageTypes = new List<ImageType>();
|
||||||
|
|
||||||
for (var i = 0; i < _singularImages.Length; i++)
|
for (var i = 0; i < _singularImages.Length; i++)
|
||||||
{
|
{
|
||||||
var type = _singularImages[i];
|
var type = _singularImages[i];
|
||||||
var image = GetFirstLocalImageInfoByType(images, type);
|
var image = GetFirstLocalImageInfoByType(images, type);
|
||||||
|
|
||||||
if (image is not null)
|
if (image is not null)
|
||||||
{
|
{
|
||||||
var currentImage = item.GetImageInfo(type, 0);
|
var currentImage = item.GetImageInfo(type, 0);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -93,10 +92,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localImagesFailed = false;
|
|
||||||
|
|
||||||
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
|
||||||
|
|
||||||
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
||||||
{
|
{
|
||||||
if (ImageProvider.RemoveImages(item))
|
if (ImageProvider.RemoveImages(item))
|
||||||
@@ -105,24 +100,35 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start by validating images
|
var localImagesFailed = false;
|
||||||
try
|
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
||||||
|
|
||||||
|
// Only validate already registered images if we are replacing and saving locally
|
||||||
|
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
|
||||||
{
|
{
|
||||||
// Always validate images and check for new locally stored ones.
|
item.ValidateImages();
|
||||||
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
|
||||||
{
|
|
||||||
updateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
localImagesFailed = true;
|
// Run full image validation and register new local images
|
||||||
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
try
|
||||||
|
{
|
||||||
|
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
||||||
|
{
|
||||||
|
updateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
localImagesFailed = true;
|
||||||
|
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadataResult = new MetadataResult<TItemType>
|
var metadataResult = new MetadataResult<TItemType>
|
||||||
{
|
{
|
||||||
Item = itemOfType
|
Item = itemOfType,
|
||||||
|
People = LibraryManager.GetPeople(item)
|
||||||
};
|
};
|
||||||
|
|
||||||
bool hasRefreshedMetadata = true;
|
bool hasRefreshedMetadata = true;
|
||||||
@@ -154,7 +160,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
id.IsAutomated = refreshOptions.IsAutomated;
|
id.IsAutomated = refreshOptions.IsAutomated;
|
||||||
|
|
||||||
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
|
var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
|
||||||
|
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
updateType |= result.UpdateType;
|
updateType |= result.UpdateType;
|
||||||
if (result.Failures > 0)
|
if (result.Failures > 0)
|
||||||
@@ -165,7 +172,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 +250,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 +406,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)
|
||||||
@@ -638,6 +646,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
MetadataRefreshOptions options,
|
MetadataRefreshOptions options,
|
||||||
ICollection<IMetadataProvider> providers,
|
ICollection<IMetadataProvider> providers,
|
||||||
ItemImageProvider imageService,
|
ItemImageProvider imageService,
|
||||||
|
bool isSavingMetadata,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var refreshResult = new RefreshResult
|
var refreshResult = new RefreshResult
|
||||||
@@ -655,102 +664,96 @@ 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;
|
||||||
|
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
||||||
|
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
||||||
|
|
||||||
// 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>>())
|
// Do not execute local providers if we are identifying or replacing with local metadata saving enabled
|
||||||
|
if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
|
||||||
|
|
||||||
var itemInfo = new ItemInfo(item);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
var providerName = provider.GetType().Name;
|
||||||
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
|
|
||||||
if (localItem.HasMetadata)
|
var itemInfo = new ItemInfo(item);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var remoteImage in localItem.RemoteImages)
|
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (localItem.HasMetadata)
|
||||||
{
|
{
|
||||||
try
|
foreach (var remoteImage in localItem.RemoteImages)
|
||||||
{
|
{
|
||||||
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
try
|
||||||
&& !options.IsReplacingImage(remoteImage.Type))
|
|
||||||
{
|
{
|
||||||
continue;
|
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
||||||
|
&& !options.IsReplacingImage(remoteImage.Type))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
|
||||||
|
// remember imagetype that has just been downloaded
|
||||||
|
foundImageTypes.Add(remoteImage.Type);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
|
|
||||||
// remember imagetype that has just been downloaded
|
|
||||||
foundImageTypes.Add(remoteImage.Type);
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
|
||||||
|
if (foundImageTypes.Count > 0)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
imageService.UpdateReplaceImages(options, foundImageTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageService.MergeImages(item, localItem.Images, options))
|
||||||
|
{
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundImageTypes.Count > 0)
|
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
|
||||||
{
|
|
||||||
imageService.UpdateReplaceImages(options, foundImageTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageService.MergeImages(item, localItem.Images, options))
|
|
||||||
{
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
|
||||||
|
|
||||||
// Only one local provider allowed per item
|
|
||||||
if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
|
|
||||||
{
|
|
||||||
hasLocalMetadata = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error in {Provider}", provider.Name);
|
||||||
|
|
||||||
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
|
// If a local provider fails, consider that a failure
|
||||||
}
|
refreshResult.ErrorMessage = ex.Message;
|
||||||
catch (OperationCanceledException)
|
}
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error in {Provider}", provider.Name);
|
|
||||||
|
|
||||||
// If a local provider fails, consider that a failure
|
|
||||||
refreshResult.ErrorMessage = ex.Message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 +765,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, true);
|
||||||
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 +791,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,23 +815,20 @@ 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();
|
||||||
|
|
||||||
var tmpDataMerged = false;
|
if (id is not null)
|
||||||
|
{
|
||||||
|
MergeNewData(temp.Item, id);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var provider in providers)
|
foreach (var provider in providers)
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var providerName = provider.GetType().Name;
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
|
|
||||||
if (id is not null && !tmpDataMerged)
|
|
||||||
{
|
|
||||||
MergeNewData(temp.Item, id);
|
|
||||||
tmpDataMerged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -846,7 +837,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 +940,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 +1003,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 +1036,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 +1048,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 +1060,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
target.ProductionLocations = source.ProductionLocations;
|
target.ProductionLocations = source.ProductionLocations;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var id in source.ProviderIds)
|
foreach (var id in source.ProviderIds)
|
||||||
@@ -1082,17 +1081,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).DistinctBy(t => t.Url).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 +1110,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 +1176,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 +1193,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 +1204,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
|
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
|
||||||
|
{
|
||||||
|
return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
|
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
|
||||||
where T : BaseItem
|
where T : BaseItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -15,6 +17,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 +30,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 +38,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 +46,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 +57,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;
|
||||||
@@ -130,6 +137,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
if (!audio.IsLocked)
|
if (!audio.IsLocked)
|
||||||
{
|
{
|
||||||
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
||||||
|
if (tryExtractEmbeddedLyrics)
|
||||||
|
{
|
||||||
|
AddExternalLyrics(audio, mediaStreams, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
||||||
@@ -146,191 +157,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 = tags.AlbumArtists.Length == 0 ? mediaInfo.AlbumArtists : tags.AlbumArtists;
|
||||||
|
tags.Album ??= mediaInfo.Album;
|
||||||
|
tags.Title ??= mediaInfo.Name;
|
||||||
|
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
||||||
|
tags.Performers = tags.Performers.Length == 0 ? mediaInfo.Artists : tags.Performers;
|
||||||
|
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(
|
||||||
@@ -342,7 +374,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
||||||
|
|
||||||
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
||||||
currentStreams.AddRange(externalLyricFiles);
|
if (externalLyricFiles.Count > 0)
|
||||||
|
{
|
||||||
|
currentStreams.Add(externalLyricFiles[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
||||||
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
||||||
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
||||||
|
blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
|
||||||
|
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
|
||||||
|
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
|
||||||
|
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,184 +1,227 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
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;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Playlists
|
namespace MediaBrowser.Providers.Playlists;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Local playlist provider.
|
||||||
|
/// </summary>
|
||||||
|
public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
|
||||||
|
IHasOrder,
|
||||||
|
IForcedProvider,
|
||||||
|
IHasItemChangeMonitor
|
||||||
{
|
{
|
||||||
public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>,
|
private readonly IFileSystem _fileSystem;
|
||||||
IHasOrder,
|
private readonly ILibraryManager _libraryManager;
|
||||||
IForcedProvider,
|
private readonly ILogger<PlaylistItemsProvider> _logger;
|
||||||
IPreRefreshProvider,
|
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
||||||
IHasItemChangeMonitor
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
private readonly ILogger<PlaylistItemsProvider> _logger;
|
_logger = logger;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger)
|
/// <inheritdoc />
|
||||||
|
public string Name => "Playlist Item Provider";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Order => 100;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<MetadataResult<Playlist>> GetMetadata(
|
||||||
|
ItemInfo info,
|
||||||
|
IDirectoryService directoryService,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new MetadataResult<Playlist>()
|
||||||
{
|
{
|
||||||
_logger = logger;
|
Item = new Playlist
|
||||||
|
{
|
||||||
|
Path = info.Path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Fetch(result);
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fetch(MetadataResult<Playlist> result)
|
||||||
|
{
|
||||||
|
var item = result.Item;
|
||||||
|
var path = item.Path;
|
||||||
|
if (!Playlist.IsPlaylistFile(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Playlist Reader";
|
var extension = Path.GetExtension(path);
|
||||||
|
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||||
// Run last
|
|
||||||
public int Order => 100;
|
|
||||||
|
|
||||||
public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var path = item.Path;
|
return;
|
||||||
if (!Playlist.IsPlaylistFile(path))
|
}
|
||||||
{
|
|
||||||
return Task.FromResult(ItemUpdateType.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
|
||||||
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return Task.FromResult(ItemUpdateType.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
var items = GetItems(path, extension).ToArray();
|
|
||||||
|
|
||||||
|
var items = GetItems(path, extension).ToArray();
|
||||||
|
if (items.Length > 0)
|
||||||
|
{
|
||||||
|
result.HasMetadata = true;
|
||||||
item.LinkedChildren = items;
|
item.LinkedChildren = items;
|
||||||
|
|
||||||
return Task.FromResult(ItemUpdateType.None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, libraryRoots);
|
||||||
{
|
|
||||||
return GetWplItems(stream, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetZplItems(stream, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetM3uItems(stream, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetM3u8Items(stream, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetPlsItems(stream, path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Enumerable.Empty<LinkedChild>();
|
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string path)
|
|
||||||
{
|
|
||||||
var content = new PlsContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
|
||||||
{
|
{
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
return GetZplItems(stream, path, libraryRoots);
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetM3u8Items(Stream stream, string path)
|
|
||||||
{
|
|
||||||
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> GetM3uItems(Stream stream, string path)
|
|
||||||
{
|
|
||||||
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 playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
|
||||||
{
|
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string path)
|
|
||||||
{
|
|
||||||
var content = new WplContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries.Select(i => new LinkedChild
|
|
||||||
{
|
|
||||||
Path = GetPlaylistItemPath(i.Path, path),
|
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetPlaylistItemPath(string itemPath, string containingPlaylistFolder)
|
|
||||||
{
|
|
||||||
if (!File.Exists(itemPath))
|
|
||||||
{
|
|
||||||
var path = Path.Combine(Path.GetDirectoryName(containingPlaylistFolder), itemPath);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return itemPath;
|
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
|
||||||
{
|
|
||||||
var path = item.Path;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
|
||||||
{
|
{
|
||||||
var file = directoryService.GetFile(path);
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetPlsItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enumerable.Empty<LinkedChild>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new PlsContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new M3uContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new ZplContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new WplContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
|
||||||
|
{
|
||||||
|
return new LinkedChild
|
||||||
|
{
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var libraryPath in libraryPaths)
|
||||||
|
{
|
||||||
|
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
path = pathToCheck;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||||
|
{
|
||||||
|
var path = item.Path;
|
||||||
|
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
||||||
|
{
|
||||||
|
var file = directoryService.GetFile(path);
|
||||||
|
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
|||||||
// If we have a release ID but not a release group ID, lookup the release group
|
// If we have a release ID but not a release group ID, lookup the release group
|
||||||
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
||||||
{
|
{
|
||||||
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false);
|
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
||||||
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
||||||
result.HasMetadata = true;
|
result.HasMetadata = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -61,24 +61,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
RemoveObsoleteEpisodes(item);
|
RemoveObsoleteEpisodes(item);
|
||||||
|
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
RemoveObsoleteSeasons(item);
|
RemoveObsoleteSeasons(item);
|
||||||
await UpdateAndCreateSeasonsAsync(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))
|
||||||
{
|
{
|
||||||
@@ -121,7 +91,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteSeasons(Series series)
|
private void RemoveObsoleteSeasons(Series series)
|
||||||
{
|
{
|
||||||
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
|
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
|
||||||
var physicalSeasonNumbers = new HashSet<int>();
|
var physicalSeasonNumbers = new HashSet<int>();
|
||||||
var virtualSeasons = new List<Season>();
|
var virtualSeasons = new List<Season>();
|
||||||
foreach (var existingSeason in series.Children.OfType<Season>())
|
foreach (var existingSeason in series.Children.OfType<Season>())
|
||||||
@@ -149,7 +119,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
virtualSeason,
|
virtualSeason,
|
||||||
new DeleteOptions
|
new DeleteOptions
|
||||||
{
|
{
|
||||||
DeleteFileLocation = true
|
// Internal metadata paths are removed regardless of this.
|
||||||
|
DeleteFileLocation = false
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
@@ -158,7 +129,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++)
|
||||||
@@ -206,7 +177,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
episode,
|
episode,
|
||||||
new DeleteOptions
|
new DeleteOptions
|
||||||
{
|
{
|
||||||
DeleteFileLocation = true
|
// Internal metadata paths are removed regardless of this.
|
||||||
|
DeleteFileLocation = false
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
@@ -214,14 +186,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
|
||||||
@@ -234,20 +204,14 @@ namespace MediaBrowser.Providers.TV
|
|||||||
{
|
{
|
||||||
// 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);
|
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
||||||
|
|
||||||
if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName))
|
|
||||||
{
|
|
||||||
seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSeason is null)
|
if (existingSeason is null)
|
||||||
{
|
{
|
||||||
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
||||||
series.AddChild(season);
|
await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal))
|
else if (existingSeason.IsVirtualItem)
|
||||||
{
|
{
|
||||||
existingSeason.Name = seasonName;
|
existingSeason.IsVirtualItem = false;
|
||||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +225,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
/// <param name="seasonNumber">The season number.</param>
|
/// <param name="seasonNumber">The season number.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The newly created season.</returns>
|
/// <returns>The newly created season.</returns>
|
||||||
private async Task<Season> CreateSeasonAsync(
|
private async Task CreateSeasonAsync(
|
||||||
Series series,
|
Series series,
|
||||||
string? seasonName,
|
string? seasonName,
|
||||||
int? seasonNumber,
|
int? seasonNumber,
|
||||||
@@ -278,14 +242,12 @@ namespace MediaBrowser.Providers.TV
|
|||||||
typeof(Season)),
|
typeof(Season)),
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeriesName = series.Name
|
SeriesName = series.Name,
|
||||||
|
SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
|
||||||
};
|
};
|
||||||
|
|
||||||
series.AddChild(season);
|
series.AddChild(season);
|
||||||
|
|
||||||
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return season;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,7 +519,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
||||||
{
|
{
|
||||||
item.PremiereDate = releaseDate;
|
item.PremiereDate = releaseDate;
|
||||||
item.ProductionYear = releaseDate.Year;
|
|
||||||
|
// Production year can already be set by the year tag
|
||||||
|
item.ProductionYear ??= releaseDate.Year;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user