Compare commits
299 Commits
master
...
feature/cu
Author | SHA1 | Date | |
---|---|---|---|
a936f76a12 | |||
203af39e26 | |||
96b133b7b4 | |||
34c63a4c15 | |||
4bb0259097 | |||
9fba024add | |||
|
7bbdfc0d49 | ||
|
3c3ebe8344 | ||
|
8c4d23435e | ||
|
9e4befa52e | ||
|
b3efae71c0 | ||
|
70f4f2e8c2 | ||
|
cd2f2ca178 | ||
|
8095d39954 | ||
|
e1ac30ba17 | ||
|
3b94cfa837 | ||
|
2fe13f54ea | ||
|
eb6a6d319b | ||
|
b74c9cae1b | ||
|
24d482b36b | ||
|
fff4477a93 | ||
|
9810d22d96 | ||
|
5027e3cd53 | ||
|
9645955629 | ||
|
078ee1f2de | ||
|
236c7649dd | ||
|
c1e52df0b7 | ||
|
be949af59e | ||
|
122da8f447 | ||
|
486c7fa51e | ||
|
6709c80f0a | ||
|
3f3145600c | ||
|
bc613b8344 | ||
|
0eb5897100 | ||
|
ee0094d889 | ||
|
7051a18be0 | ||
|
fce3a5d241 | ||
|
900acc03aa | ||
|
a475a7d50a | ||
|
b7bc0e1c96 | ||
|
3c79d7a3f3 | ||
|
1739962f52 | ||
|
7f0f93eb4a | ||
|
71c13057f4 | ||
|
7f12677dc3 | ||
|
4f2b1736ab | ||
|
c05049e54e | ||
|
dd5f6406a2 | ||
|
79c0a7d7f0 | ||
|
3c6485f0a1 | ||
|
8a8b2c4380 | ||
|
7403428864 | ||
|
235da65a75 | ||
|
26eab7aa2e | ||
|
d235378133 | ||
|
5a62c7a146 | ||
|
4afa6db108 | ||
|
f7a90b6383 | ||
|
b8e2d8e11a | ||
|
c1f7ccbca4 | ||
|
5bab02fa54 | ||
|
f2fa0b9025 | ||
|
9c0edd2905 | ||
|
62deebc04c | ||
|
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 |
|
@ -3,7 +3,7 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.4",
|
"version": "8.0.7",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|
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 }}
|
||||||
|
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -277,3 +277,5 @@ apiclient/generated
|
||||||
|
|
||||||
# Omnisharp crash logs
|
# Omnisharp crash logs
|
||||||
mono_crash.*.json
|
mono_crash.*.json
|
||||||
|
|
||||||
|
out
|
||||||
|
|
|
@ -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,40 +16,39 @@
|
||||||
<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="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" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.5" />
|
<PackageVersion Include="libse" Version="4.0.7" />
|
||||||
<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.7" />
|
||||||
<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.7" />
|
||||||
<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.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" />
|
||||||
<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" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<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.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.4" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
|
||||||
<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" />
|
||||||
|
@ -59,12 +58,12 @@
|
||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||||
|
@ -74,19 +73,19 @@
|
||||||
<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" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
|
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<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.11</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();
|
||||||
|
|
|
@ -119,5 +119,56 @@ namespace Emby.Server.Implementations.Configuration
|
||||||
EnsureWriteAccess(newPath);
|
EnsureWriteAccess(newPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
public void LogMessage(string message)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("(MJH) {Message}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSubPath(string basePath, string testPath)
|
||||||
|
{
|
||||||
|
string normalizedBasePath = Path.GetFullPath(basePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
string normalizedTestPath = Path.GetFullPath(testPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
|
||||||
|
var isSubPath = normalizedTestPath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& normalizedTestPath.Length > normalizedBasePath.Length
|
||||||
|
&& (normalizedTestPath[normalizedBasePath.Length] == Path.DirectorySeparatorChar
|
||||||
|
|| normalizedTestPath[normalizedBasePath.Length] == Path.AltDirectorySeparatorChar);
|
||||||
|
return isSubPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the folder priority.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to test.</param>
|
||||||
|
/// <returns>The priority int value (higher comes first).</returns>
|
||||||
|
public int FetchFolderPriority(string path)
|
||||||
|
{
|
||||||
|
if (this.IsSubPath("/mnt/media-pool", path))
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.IsSubPath("/mnt/lee/laptop-new", path))
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.IsSubPath("/mnt/lee/laptop-seagate", path))
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.IsSubPath("/mnt/lee/pi-elements", path))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
if (people is not null)
|
||||||
|
{
|
||||||
InsertPeople(itemId, people, connection);
|
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;
|
||||||
|
@ -5700,14 +5694,18 @@ AND Type = @InternalPersonType)");
|
||||||
|
|
||||||
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
|
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
|
||||||
|
|
||||||
if (item.Type == MediaStreamType.Subtitle)
|
if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
|
||||||
|
{
|
||||||
|
item.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||||
|
item.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||||
|
|
||||||
|
if (item.Type is MediaStreamType.Subtitle)
|
||||||
{
|
{
|
||||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||||
item.LocalizedDefault = _localization.GetLocalizedString("Default");
|
|
||||||
item.LocalizedForced = _localization.GetLocalizedString("Forced");
|
item.LocalizedForced = _localization.GetLocalizedString("Forced");
|
||||||
item.LocalizedExternal = _localization.GetLocalizedString("External");
|
|
||||||
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
@ -5728,7 +5726,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 +5776,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,9 +2812,11 @@ 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)
|
||||||
{
|
{
|
||||||
|
@ -3207,5 +3209,10 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void LogMessage(string message)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("(MJH) {Message}", message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,18 +79,9 @@ 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))
|
|
||||||
{
|
|
||||||
var seasonNames = series.SeasonNames;
|
|
||||||
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
|
|
||||||
{
|
|
||||||
season.Name = seasonName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
season.Name = seasonNumber == 0 ?
|
season.Name = seasonNumber == 0 ?
|
||||||
args.LibraryOptions.SeasonZeroDisplayName :
|
args.LibraryOptions.SeasonZeroDisplayName :
|
||||||
string.Format(
|
string.Format(
|
||||||
|
@ -98,8 +90,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
args.LibraryOptions.PreferredMetadataLanguage);
|
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());
|
||||||
|
|
||||||
|
var ratingLevelRightPart = ratingSpan.RightPart('-');
|
||||||
|
if (ratingLevelRightPart.Length != 0)
|
||||||
|
{
|
||||||
// Check rating system of culture
|
// Check rating system of culture
|
||||||
return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
|
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,14 +106,21 @@ 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);
|
||||||
|
try
|
||||||
|
{
|
||||||
a.LUFS = await CalculateLUFSAsync(
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
File.Delete(tempFile);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||||
|
|
||||||
var activityDate = DateTime.UtcNow;
|
var activityDate = DateTime.UtcNow;
|
||||||
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
||||||
var lastActivityDate = session.LastActivityDate;
|
var lastActivityDate = session.LastActivityDate;
|
||||||
session.LastActivityDate = activityDate;
|
session.LastActivityDate = activityDate;
|
||||||
|
|
||||||
|
@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <returns>SessionInfo.</returns>
|
/// <returns>SessionInfo.</returns>
|
||||||
private async Task<SessionInfo> GetSessionInfo(
|
private SessionInfo GetSessionInfo(
|
||||||
string appName,
|
string appName,
|
||||||
string appVersion,
|
string appVersion,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
|
@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
|
|
||||||
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
||||||
{
|
{
|
||||||
sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
||||||
_activeConnections[key] = sessionInfo;
|
_activeConnections[key] = sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
return sessionInfo;
|
return sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SessionInfo> CreateSession(
|
private SessionInfo CreateSession(
|
||||||
string key,
|
string key,
|
||||||
string appName,
|
string appName,
|
||||||
string appVersion,
|
string appVersion,
|
||||||
|
@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
deviceName = "Network Device";
|
deviceName = "Network Device";
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
|
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
|
||||||
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
||||||
{
|
{
|
||||||
sessionInfo.DeviceName = deviceName;
|
sessionInfo.DeviceName = deviceName;
|
||||||
|
@ -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();
|
||||||
|
@ -1296,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
return new[] { item };
|
return new[] { item };
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(id);
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
|
||||||
|
@ -1306,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
return new List<BaseItem>();
|
return new List<BaseItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
|
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1519,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
|
||||||
// This should be validated above, but if it isn't don't delete all tokens.
|
// This should be validated above, but if it isn't don't delete all tokens.
|
||||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||||
|
|
||||||
var existing = (await _deviceManager.GetDevices(
|
var existing = _deviceManager.GetDevices(
|
||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
UserId = user.Id
|
UserId = user.Id
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
foreach (var auth in existing)
|
foreach (var auth in existing)
|
||||||
{
|
{
|
||||||
|
@ -1552,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
|
||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
||||||
|
|
||||||
var existing = (await _deviceManager.GetDevices(
|
var existing = _deviceManager.GetDevices(
|
||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
AccessToken = accessToken
|
AccessToken = accessToken
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
if (existing.Count > 0)
|
if (existing.Count > 0)
|
||||||
{
|
{
|
||||||
|
@ -1596,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var existing = await _deviceManager.GetDevices(new DeviceQuery
|
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
UserId = userId
|
UserId = userId
|
||||||
}).ConfigureAwait(false);
|
});
|
||||||
|
|
||||||
foreach (var info in existing.Items)
|
foreach (var info in existing.Items)
|
||||||
{
|
{
|
||||||
|
@ -1786,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
||||||
{
|
{
|
||||||
var items = (await _deviceManager.GetDevices(new DeviceQuery
|
var items = _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
AccessToken = token,
|
AccessToken = token,
|
||||||
Limit = 1
|
Limit = 1
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
if (items.Count == 0)
|
if (items.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
|
||||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
|
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
|
return _deviceManager.GetDevicesForUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
|
||||||
[HttpGet("Info")]
|
[HttpGet("Info")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
var deviceInfo = _deviceManager.GetDevice(id);
|
||||||
if (deviceInfo is null)
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
|
||||||
[HttpGet("Options")]
|
[HttpGet("Options")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
||||||
if (deviceInfo is null)
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
var existingDevice = _deviceManager.GetDevice(id);
|
||||||
if (existingDevice is null)
|
if (existingDevice is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
|
||||||
|
|
||||||
foreach (var session in sessions.Items)
|
foreach (var session in sessions.Items)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2089,6 +2089,8 @@ public class ImageController : BaseJellyfinApiController
|
||||||
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||||
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
||||||
|
|
||||||
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
|
|
||||||
if (disableCaching)
|
if (disableCaching)
|
||||||
{
|
{
|
||||||
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -289,18 +289,36 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||||
if (item is Series rseries)
|
if (item is Series rseries)
|
||||||
{
|
{
|
||||||
foreach (var season in rseries.Children.OfType<Season>())
|
foreach (var season in rseries.Children.OfType<Season>())
|
||||||
|
{
|
||||||
|
if (!season.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
{
|
{
|
||||||
season.OfficialRating = request.OfficialRating;
|
season.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
season.CustomRating = request.CustomRating;
|
season.CustomRating = request.CustomRating;
|
||||||
|
|
||||||
|
if (!season.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
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>())
|
||||||
|
{
|
||||||
|
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
{
|
{
|
||||||
ep.OfficialRating = request.OfficialRating;
|
ep.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
ep.CustomRating = request.CustomRating;
|
ep.CustomRating = request.CustomRating;
|
||||||
|
|
||||||
|
if (!ep.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
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);
|
||||||
}
|
}
|
||||||
|
@ -309,10 +327,19 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||||
else if (item is Season season)
|
else if (item is Season season)
|
||||||
{
|
{
|
||||||
foreach (var ep in season.Children.OfType<Episode>())
|
foreach (var ep in season.Children.OfType<Episode>())
|
||||||
|
{
|
||||||
|
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
{
|
{
|
||||||
ep.OfficialRating = request.OfficialRating;
|
ep.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
ep.CustomRating = request.CustomRating;
|
ep.CustomRating = request.CustomRating;
|
||||||
|
|
||||||
|
if (!ep.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
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);
|
||||||
}
|
}
|
||||||
|
@ -320,10 +347,19 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||||
else if (item is MusicAlbum album)
|
else if (item is MusicAlbum album)
|
||||||
{
|
{
|
||||||
foreach (BaseItem track in album.Children)
|
foreach (BaseItem track in album.Children)
|
||||||
|
{
|
||||||
|
if (!track.LockedFields.Contains(MetadataField.OfficialRating))
|
||||||
{
|
{
|
||||||
track.OfficialRating = request.OfficialRating;
|
track.OfficialRating = request.OfficialRating;
|
||||||
|
}
|
||||||
|
|
||||||
track.CustomRating = request.CustomRating;
|
track.CustomRating = request.CustomRating;
|
||||||
|
|
||||||
|
if (!track.LockedFields.Contains(MetadataField.Tags))
|
||||||
|
{
|
||||||
track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
|
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,8 +180,22 @@ 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.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);
|
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Need to add a delay here or directory watchers may still pick up the changes
|
// Need to add a delay here or directory watchers may still pick up the changes
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
|
|
||||||
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
||||||
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
|
||||||
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
||||||
if (System.IO.File.Exists(path))
|
if (System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -173,6 +174,19 @@ public class VideosController : BaseJellyfinApiController
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSubPath(string basePath, string testPath)
|
||||||
|
{
|
||||||
|
string normalizedBasePath = Path.GetFullPath(basePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
string normalizedTestPath = Path.GetFullPath(testPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
|
||||||
|
var isSubPath = normalizedTestPath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& normalizedTestPath.Length > normalizedBasePath.Length
|
||||||
|
&& (normalizedTestPath[normalizedBasePath.Length] == Path.DirectorySeparatorChar
|
||||||
|
|| normalizedTestPath[normalizedBasePath.Length] == Path.AltDirectorySeparatorChar);
|
||||||
|
|
||||||
|
return isSubPath;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges videos into a single record.
|
/// Merges videos into a single record.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -198,11 +212,21 @@ public class VideosController : BaseJellyfinApiController
|
||||||
return BadRequest("Please supply at least two videos to merge.");
|
return BadRequest("Please supply at least two videos to merge.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
|
var primaryVersion = items.FirstOrDefault(i =>
|
||||||
|
{
|
||||||
|
if (i.GetMediaSourceCount((message) => _serverConfigurationManager.LogMessage(message)) > 1 && string.IsNullOrEmpty(i.PrimaryVersionId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
if (primaryVersion is null)
|
if (primaryVersion is null)
|
||||||
{
|
{
|
||||||
primaryVersion = items
|
primaryVersion = items
|
||||||
.OrderBy(i =>
|
.OrderBy(i => _serverConfigurationManager.FetchFolderPriority(i.GetContainingFolderPath()))
|
||||||
|
.ThenBy(i =>
|
||||||
{
|
{
|
||||||
if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
|
if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.11</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>
|
||||||
|
|
|
@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
||||||
|
private readonly ConcurrentDictionary<int, Device> _devices;
|
||||||
|
private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
||||||
|
@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
{
|
{
|
||||||
_dbProvider = dbProvider;
|
_dbProvider = dbProvider;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_devices = new ConcurrentDictionary<int, Device>();
|
||||||
|
_deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
|
||||||
|
|
||||||
|
using var dbContext = _dbProvider.CreateDbContext();
|
||||||
|
foreach (var device in dbContext.Devices
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
|
.AsEnumerable())
|
||||||
|
{
|
||||||
|
_devices.TryAdd(device.Id, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var deviceOption in dbContext.DeviceOptions
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
|
.AsEnumerable())
|
||||||
|
{
|
||||||
|
_deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_deviceOptions[deviceId] = deviceOptions;
|
||||||
|
|
||||||
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,25 +97,17 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
dbContext.Devices.Add(device);
|
dbContext.Devices.Add(device);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
_devices.TryAdd(device.Id, device);
|
||||||
}
|
}
|
||||||
|
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
|
public DeviceOptions GetDeviceOptions(string deviceId)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
_deviceOptions.TryGetValue(deviceId, out var deviceOptions);
|
||||||
DeviceOptions? deviceOptions;
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
deviceOptions = await dbContext.DeviceOptions
|
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(d => d.DeviceId == deviceId)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceOptions ?? new DeviceOptions(deviceId);
|
return deviceOptions ?? new DeviceOptions(deviceId);
|
||||||
}
|
}
|
||||||
|
@ -108,38 +121,25 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DeviceInfo?> GetDevice(string id)
|
public DeviceInfo? GetDevice(string id)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
|
||||||
await using (dbContext.ConfigureAwait(false))
|
_deviceOptions.TryGetValue(id, out var deviceOption);
|
||||||
{
|
|
||||||
var device = await dbContext.Devices
|
|
||||||
.Where(d => d.DeviceId == id)
|
|
||||||
.OrderByDescending(d => d.DateLastActivity)
|
|
||||||
.Include(d => d.User)
|
|
||||||
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
|
|
||||||
|
|
||||||
|
var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
|
||||||
return deviceInfo;
|
return deviceInfo;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
|
public QueryResult<Device> GetDevices(DeviceQuery query)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
IEnumerable<Device> devices = _devices.Values
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
var devices = dbContext.Devices
|
|
||||||
.OrderBy(d => d.Id)
|
|
||||||
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
|
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
|
||||||
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
|
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
|
||||||
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
|
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
var count = await devices.CountAsync().ConfigureAwait(false);
|
.ToList();
|
||||||
|
var count = devices.Count();
|
||||||
|
|
||||||
if (query.Skip.HasValue)
|
if (query.Skip.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -151,14 +151,13 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
devices = devices.Take(query.Limit.Value);
|
devices = devices.Take(query.Limit.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
|
return new QueryResult<Device>(query.Skip, count, devices.ToList());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
|
public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
|
||||||
{
|
{
|
||||||
var devices = await GetDevices(query).ConfigureAwait(false);
|
var devices = GetDevices(query);
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(
|
return new QueryResult<DeviceInfo>(
|
||||||
devices.StartIndex,
|
devices.StartIndex,
|
||||||
|
@ -167,19 +166,13 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
|
public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
IEnumerable<Device> devices = _devices.Values
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
var sessions = dbContext.Devices
|
|
||||||
.Include(d => d.User)
|
|
||||||
.OrderByDescending(d => d.DateLastActivity)
|
.OrderByDescending(d => d.DateLastActivity)
|
||||||
.ThenBy(d => d.DeviceId)
|
.ThenBy(d => d.DeviceId);
|
||||||
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
|
||||||
.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)
|
||||||
|
@ -187,18 +180,22 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
throw new ResourceNotFoundException();
|
throw new ResourceNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
|
devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
|
var array = devices.Select(device =>
|
||||||
|
{
|
||||||
|
_deviceOptions.TryGetValue(device.DeviceId, out var option);
|
||||||
|
return ToDeviceInfo(device, option);
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(array);
|
return new QueryResult<DeviceInfo>(array);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteDevice(Device device)
|
public async Task DeleteDevice(Device device)
|
||||||
{
|
{
|
||||||
|
_devices.TryRemove(device.Id, out _);
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
@ -207,6 +204,19 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task UpdateDevice(Device device)
|
||||||
|
{
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
dbContext.Devices.Update(device);
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_devices[device.Id] = device;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool CanAccessDevice(User user, string deviceId)
|
public bool CanAccessDevice(User user, string deviceId)
|
||||||
{
|
{
|
||||||
|
@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
||||||
{
|
{
|
||||||
var caps = GetCapabilities(authInfo.DeviceId);
|
var caps = GetCapabilities(authInfo.DeviceId);
|
||||||
|
var user = _userManager.GetUserById(authInfo.UserId);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
|
||||||
|
}
|
||||||
|
|
||||||
return new DeviceInfo
|
return new DeviceInfo
|
||||||
{
|
{
|
||||||
|
@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||||
AppVersion = authInfo.AppVersion,
|
AppVersion = authInfo.AppVersion,
|
||||||
Id = authInfo.DeviceId,
|
Id = authInfo.DeviceId,
|
||||||
LastUserId = authInfo.UserId,
|
LastUserId = authInfo.UserId,
|
||||||
LastUserName = authInfo.User.Username,
|
LastUserName = user.Username,
|
||||||
Name = authInfo.DeviceName,
|
Name = authInfo.DeviceName,
|
||||||
DateLastActivity = authInfo.DateLastActivity,
|
DateLastActivity = authInfo.DateLastActivity,
|
||||||
IconUrl = caps.IconUrl,
|
IconUrl = caps.IconUrl,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using EFCoreSecondLevelCacheInterceptor;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
@ -19,18 +18,10 @@ public static class ServiceCollectionExtensions
|
||||||
/// <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)
|
||||||
{
|
{
|
||||||
serviceCollection.AddEFSecondLevelCache(options =>
|
|
||||||
options.UseMemoryCacheProvider()
|
|
||||||
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
|
|
||||||
.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")}")
|
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||||
.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceCollection;
|
return serviceCollection;
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
|
|
||||||
<PackageReference Include="System.Linq.Async" />
|
<PackageReference Include="System.Linq.Async" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
|
|
@ -4,7 +4,10 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Queries;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly IServerApplicationHost _serverApplicationHost;
|
private readonly IServerApplicationHost _serverApplicationHost;
|
||||||
|
|
||||||
public AuthorizationContext(
|
public AuthorizationContext(
|
||||||
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
IDeviceManager deviceManager,
|
||||||
IServerApplicationHost serverApplicationHost)
|
IServerApplicationHost serverApplicationHost)
|
||||||
{
|
{
|
||||||
_jellyfinDbProvider = jellyfinDb;
|
_jellyfinDbProvider = jellyfinDb;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_deviceManager = deviceManager;
|
||||||
_serverApplicationHost = serverApplicationHost;
|
_serverApplicationHost = serverApplicationHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
|
var device = _deviceManager.GetDevices(
|
||||||
|
new DeviceQuery
|
||||||
|
{
|
||||||
|
AccessToken = token
|
||||||
|
}).Items.FirstOrDefault();
|
||||||
|
|
||||||
if (device is not null)
|
if (device is not null)
|
||||||
{
|
{
|
||||||
|
@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
|
||||||
|
|
||||||
if (updateToken)
|
if (updateToken)
|
||||||
{
|
{
|
||||||
dbContext.Devices.Update(device);
|
await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
|
||||||
|
|
||||||
private async Task UpdateDeviceAccess(User user)
|
private async Task UpdateDeviceAccess(User user)
|
||||||
{
|
{
|
||||||
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
UserId = user.Id
|
UserId = user.Id
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
foreach (var device in existing)
|
foreach (var device in existing)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,21 +237,12 @@ 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))
|
||||||
|
|
||||||
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));
|
throw new ResourceNotFoundException(nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
|
if (_users.Count == 1)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(string.Format(
|
throw new InvalidOperationException(string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -259,9 +251,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.HasPermission(PermissionKind.IsAdministrator)
|
if (user.HasPermission(PermissionKind.IsAdministrator)
|
||||||
&& await dbContext.Users
|
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
||||||
.CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
|
|
||||||
.ConfigureAwait(false) == 1)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
string.Format(
|
string.Format(
|
||||||
|
@ -271,12 +261,17 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
nameof(userId));
|
nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
dbContext.Users.Remove(user);
|
dbContext.Users.Remove(user);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_users.Remove(userId);
|
||||||
|
|
||||||
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task ResetPassword(User user)
|
public Task ResetPassword(User user)
|
||||||
|
@ -541,12 +536,9 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||||
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
|
if (_users.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -559,6 +551,9 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
|
|
||||||
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
||||||
|
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.11</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>
|
||||||
|
|
|
@ -19,5 +19,18 @@ namespace MediaBrowser.Controller.Configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The configuration.</value>
|
/// <value>The configuration.</value>
|
||||||
ServerConfiguration Configuration { get; }
|
ServerConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the folder priority.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to test.</param>
|
||||||
|
/// <returns>The priority int value (higher comes first).</returns>
|
||||||
|
int FetchFolderPriority(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
void LogMessage(string message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The identifier.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <returns>DeviceInfo.</returns>
|
/// <returns>DeviceInfo.</returns>
|
||||||
Task<DeviceInfo> GetDevice(string id);
|
DeviceInfo GetDevice(string id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets devices based on the provided query.
|
/// Gets devices based on the provided query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The device query.</param>
|
/// <param name="query">The device query.</param>
|
||||||
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
||||||
Task<QueryResult<Device>> GetDevices(DeviceQuery query);
|
QueryResult<Device> GetDevices(DeviceQuery query);
|
||||||
|
|
||||||
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
|
QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the devices.
|
/// Gets the devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
||||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||||
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
|
QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
|
||||||
|
|
||||||
Task DeleteDevice(Device device);
|
Task DeleteDevice(Device device);
|
||||||
|
|
||||||
|
Task UpdateDevice(Device device);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether this instance [can access device] the specified user identifier.
|
/// Determines whether this instance [can access device] the specified user identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
|
||||||
|
|
||||||
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
||||||
|
|
||||||
Task<DeviceOptions> GetDeviceOptions(string deviceId);
|
DeviceOptions GetDeviceOptions(string deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -1183,28 +1180,29 @@ namespace MediaBrowser.Controller.Entities
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMediaSourceName(BaseItem item)
|
internal string GetMediaSourceName(BaseItem item)
|
||||||
{
|
{
|
||||||
var terms = new List<string>();
|
var terms = new List<string>();
|
||||||
|
|
||||||
var path = item.Path;
|
var path = item.Path;
|
||||||
if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
|
if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
|
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||||
if (HasLocalAlternateVersions)
|
if (HasLocalAlternateVersions)
|
||||||
{
|
{
|
||||||
var displayName = System.IO.Path.GetFileNameWithoutExtension(path)
|
var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
|
||||||
.Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase)
|
if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase))
|
||||||
.TrimStart(new char[] { ' ', '-' });
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(displayName))
|
|
||||||
{
|
{
|
||||||
terms.Add(displayName);
|
var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
|
||||||
|
if (!name.IsWhiteSpace())
|
||||||
|
{
|
||||||
|
terms.Add(name.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terms.Count == 0)
|
if (terms.Count == 0)
|
||||||
{
|
{
|
||||||
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
|
|
||||||
terms.Add(displayName);
|
terms.Add(displayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1952,14 +1950,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
|
||||||
|
|
|
@ -180,10 +180,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FindSeriesPresentationUniqueKey()
|
public string FindSeriesPresentationUniqueKey()
|
||||||
{
|
=> Series?.PresentationUniqueKey;
|
||||||
var series = Series;
|
|
||||||
return series is null ? null : series.PresentationUniqueKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FindSeasonName()
|
public string FindSeasonName()
|
||||||
{
|
{
|
||||||
|
|
|
@ -159,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
|
|
||||||
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
||||||
|
|
||||||
var items = GetEpisodes(user, query.DtoOptions).Where(filter);
|
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
|
||||||
|
|
||||||
return PostFilterAndSort(items, query, false);
|
return PostFilterAndSort(items, query, false);
|
||||||
}
|
}
|
||||||
|
@ -169,30 +169,31 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <param name="options">The options to use.</param>
|
/// <param name="options">The options to use.</param>
|
||||||
|
/// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
|
||||||
/// <returns>Set of episodes.</returns>
|
/// <returns>Set of episodes.</returns>
|
||||||
public List<BaseItem> GetEpisodes(User user, DtoOptions options)
|
public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
return GetEpisodes(Series, user, options);
|
return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options)
|
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
return GetEpisodes(series, user, null, options);
|
return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options)
|
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options);
|
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes()
|
public List<BaseItem> GetEpisodes()
|
||||||
{
|
{
|
||||||
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true));
|
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
return GetEpisodes(user, new DtoOptions(true));
|
return GetEpisodes(user, new DtoOptions(true), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool GetBlockUnratedValue(User user)
|
protected override bool GetBlockUnratedValue(User user)
|
||||||
|
|
|
@ -28,16 +28,12 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
public Series()
|
public Series()
|
||||||
{
|
{
|
||||||
AirDays = Array.Empty<DayOfWeek>();
|
AirDays = Array.Empty<DayOfWeek>();
|
||||||
SeasonNames = new Dictionary<int, string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DayOfWeek[] AirDays { get; set; }
|
public DayOfWeek[] AirDays { get; set; }
|
||||||
|
|
||||||
public string AirTime { get; set; }
|
public string AirTime { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Dictionary<int, string> SeasonNames { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override bool SupportsAddingToPlaylist => true;
|
public override bool SupportsAddingToPlaylist => true;
|
||||||
|
|
||||||
|
@ -73,9 +69,6 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
/// <value>The status.</value>
|
/// <value>The status.</value>
|
||||||
public SeriesStatus? Status { get; set; }
|
public SeriesStatus? Status { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool StopRefreshIfLocalMetadataFound => false;
|
|
||||||
|
|
||||||
public override double GetDefaultPrimaryImageAspectRatio()
|
public override double GetDefaultPrimaryImageAspectRatio()
|
||||||
{
|
{
|
||||||
double value = 2;
|
double value = 2;
|
||||||
|
@ -257,7 +250,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
return LibraryManager.GetItemsResult(query);
|
return LibraryManager.GetItemsResult(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
|
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
var seriesKey = GetUniqueSeriesKey(this);
|
var seriesKey = GetUniqueSeriesKey(this);
|
||||||
|
|
||||||
|
@ -267,10 +260,10 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
|
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||||
DtoOptions = options
|
DtoOptions = options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (user is null || !user.DisplayMissingEpisodes)
|
if (!shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
query.IsMissing = false;
|
query.IsMissing = false;
|
||||||
}
|
}
|
||||||
|
@ -280,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
|
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
|
||||||
|
|
||||||
var allEpisodes = allItems.OfType<Season>()
|
var allEpisodes = allItems.OfType<Season>()
|
||||||
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
|
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
|
||||||
.Reverse();
|
.Reverse();
|
||||||
|
|
||||||
// Specials could appear twice based on above - once in season 0, once in the aired season
|
// Specials could appear twice based on above - once in season 0, once in the aired season
|
||||||
|
@ -292,8 +285,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
|
|
||||||
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Refresh bottom up, children first, then the boxset
|
// Refresh bottom up, seasons and episodes first, then the series
|
||||||
// By then hopefully the movies within will have Tmdb collection values
|
|
||||||
var items = GetRecursiveChildren();
|
var items = GetRecursiveChildren();
|
||||||
|
|
||||||
var totalItems = items.Count;
|
var totalItems = items.Count;
|
||||||
|
@ -356,7 +348,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
|
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
||||||
|
|
||||||
|
@ -373,24 +365,22 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
if (user is not null)
|
|
||||||
{
|
if (!shouldIncludeMissingEpisodes)
|
||||||
if (!user.DisplayMissingEpisodes)
|
|
||||||
{
|
{
|
||||||
query.IsMissing = false;
|
query.IsMissing = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var allItems = LibraryManager.GetItemList(query);
|
var allItems = LibraryManager.GetItemList(query);
|
||||||
|
|
||||||
return GetSeasonEpisodes(parentSeason, user, allItems, options);
|
return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
|
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
if (allSeriesEpisodes is null)
|
if (allSeriesEpisodes is null)
|
||||||
{
|
{
|
||||||
return GetSeasonEpisodes(parentSeason, user, options);
|
return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
|
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
|
||||||
|
|
|
@ -23,9 +23,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
TrailerTypes = Array.Empty<TrailerType>();
|
TrailerTypes = Array.Empty<TrailerType>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool StopRefreshIfLocalMetadataFound => false;
|
|
||||||
|
|
||||||
public TrailerType[] TrailerTypes { get; set; }
|
public TrailerType[] TrailerTypes { get; set; }
|
||||||
|
|
||||||
public override double GetDefaultPrimaryImageAspectRatio()
|
public override double GetDefaultPrimaryImageAspectRatio()
|
||||||
|
|
|
@ -430,8 +430,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
InternalItemsQuery query,
|
InternalItemsQuery query,
|
||||||
ILibraryManager libraryManager)
|
ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
var user = query.User;
|
|
||||||
|
|
||||||
// This must be the last filter
|
// This must be the last filter
|
||||||
if (!query.AdjacentTo.IsNullOrEmpty())
|
if (!query.AdjacentTo.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
|
|
|
@ -148,22 +148,7 @@ namespace MediaBrowser.Controller.Entities
|
||||||
public override bool SupportsAddingToPlaylist => true;
|
public override bool SupportsAddingToPlaylist => true;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public int MediaSourceCount
|
public int MediaSourceCount => GetMediaSourceCount((message) => LibraryManager.LogMessage(message));
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
|
||||||
{
|
|
||||||
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
|
||||||
if (item is Video video)
|
|
||||||
{
|
|
||||||
return video.MediaSourceCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsStacked => AdditionalParts.Length > 0;
|
public bool IsStacked => AdditionalParts.Length > 0;
|
||||||
|
@ -550,5 +535,44 @@ namespace MediaBrowser.Controller.Entities
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Gets hte media source count.</summary>
|
||||||
|
/// <param name="logMessage">Logs a message.</param>
|
||||||
|
/// <returns>The mediasourcecount.</returns>
|
||||||
|
public int GetMediaSourceCount(Action<string> logMessage)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
||||||
|
{
|
||||||
|
logMessage($"Got a PrimaryVersionId: {PrimaryVersionId}, {GetContainingFolderPath()}");
|
||||||
|
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
||||||
|
if (item is Video video)
|
||||||
|
{
|
||||||
|
logMessage($"Entered the recursion!!!");
|
||||||
|
return video.MediaSourceCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Gets the containing folder path.</summary>
|
||||||
|
/// <returns>The path to the containing folder path.</returns>
|
||||||
|
public string GetContainingFolderPath()
|
||||||
|
{
|
||||||
|
if (IsStacked)
|
||||||
|
{
|
||||||
|
return System.IO.Path.GetDirectoryName(Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsPlaceHolder)
|
||||||
|
{
|
||||||
|
if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
|
||||||
|
{
|
||||||
|
return Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.ContainingFolderPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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>
|
||||||
|
@ -613,5 +621,11 @@ namespace MediaBrowser.Controller.Library
|
||||||
/// This exists so plugins can trigger a library scan.
|
/// This exists so plugins can trigger a library scan.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void QueueLibraryScan();
|
void QueueLibraryScan();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
void LogMessage(string message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.11</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))
|
||||||
|
@ -1320,7 +1313,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
|
|
||||||
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
||||||
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
||||||
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
@ -2012,7 +2005,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
|
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
|
||||||
profile = WhiteSpaceRegex().Replace(profile, string.Empty);
|
profile = WhiteSpaceRegex().Replace(profile, string.Empty).ToLowerInvariant();
|
||||||
|
|
||||||
|
var videoProfiles = Array.Empty<string>();
|
||||||
|
if (string.Equals("h264", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
videoProfiles = _videoProfilesH264;
|
||||||
|
}
|
||||||
|
else if (string.Equals("hevc", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
videoProfiles = _videoProfilesH265;
|
||||||
|
}
|
||||||
|
else if (string.Equals("av1", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
videoProfiles = _videoProfilesAv1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!videoProfiles.Contains(profile, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
profile = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
// We only transcode to HEVC 8-bit for now, force Main Profile.
|
// We only transcode to HEVC 8-bit for now, force Main Profile.
|
||||||
if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
|
if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
|
||||||
|
@ -2083,6 +2095,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
|
||||||
|
@ -2315,9 +2339,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
// Video bitrate must fall within requested value
|
// Video bitrate must fall within requested value
|
||||||
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))
|
||||||
|
{
|
||||||
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
|
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
|
||||||
if (maxBitDepth.HasValue)
|
if (maxBitDepth.HasValue)
|
||||||
|
@ -2629,10 +2657,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 +2672,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 +2798,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 +3188,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 +3219,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 +3239,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 +3251,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 +3264,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 +3277,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 +4325,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 +4335,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 +4510,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -88,6 +88,19 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
MediaSourceInfo mediaSource,
|
MediaSourceInfo mediaSource,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
|
||||||
|
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
|
||||||
|
if (shouldExtractOneByOne)
|
||||||
|
{
|
||||||
|
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||||
|
foreach (var i in attachmentIndexes)
|
||||||
|
{
|
||||||
|
var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
|
||||||
|
await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
@ -101,6 +114,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ExtractAllAttachmentsExternal(
|
public async Task ExtractAllAttachmentsExternal(
|
||||||
string inputArgument,
|
string inputArgument,
|
||||||
|
@ -270,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
|
|
||||||
if (extractableAttachmentIds.Count > 0)
|
if (extractableAttachmentIds.Count > 0)
|
||||||
{
|
{
|
||||||
await CacheAllAttachmentsInternal(mediaPath, inputFile, mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
|
await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -309,7 +323,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
|
|
||||||
processArgs += string.Format(
|
processArgs += string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
" -i \"{0}\" -t 0 -f null null",
|
" -i {0} -t 0 -f null null",
|
||||||
inputFile);
|
inputFile);
|
||||||
|
|
||||||
int exitCode;
|
int exitCode;
|
||||||
|
|
|
@ -168,6 +168,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
|
|
||||||
private readonly string _encoderPath;
|
private readonly string _encoderPath;
|
||||||
|
|
||||||
|
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
|
||||||
|
|
||||||
public EncoderValidator(ILogger logger, string encoderPath)
|
public EncoderValidator(ILogger logger, string encoderPath)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
@ -477,7 +479,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckSupportedRuntimeKey(string keyDesc)
|
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(keyDesc))
|
if (string.IsNullOrEmpty(keyDesc))
|
||||||
{
|
{
|
||||||
|
@ -487,7 +489,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
|
// With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
|
||||||
|
var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
|
||||||
|
output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -195,7 +193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
|
|
||||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||||
|
|
||||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
|
||||||
|
|
||||||
// Check the Vaapi device vendor
|
// Check the Vaapi device vendor
|
||||||
if (OperatingSystem.IsLinux()
|
if (OperatingSystem.IsLinux()
|
||||||
|
@ -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,13 +619,13 @@ 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)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, cancellationToken).ConfigureAwait(false);
|
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, false, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
@ -639,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false);
|
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, isAudio, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetImageResolutionParameter()
|
private string GetImageResolutionParameter()
|
||||||
|
@ -665,7 +663,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
return imageResolutionParameter;
|
return imageResolutionParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken)
|
private async Task<string> ExtractImageInternal(
|
||||||
|
string inputPath,
|
||||||
|
string container,
|
||||||
|
MediaStream videoStream,
|
||||||
|
int? imageStreamIndex,
|
||||||
|
Video3DFormat? threedFormat,
|
||||||
|
TimeSpan? offset,
|
||||||
|
bool useIFrame,
|
||||||
|
ImageFormat? targetFormat,
|
||||||
|
bool isAudio,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||||
|
|
||||||
|
@ -724,7 +732,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
|
|
||||||
var vf = string.Join(',', filters);
|
var vf = string.Join(',', filters);
|
||||||
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
|
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
|
||||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter());
|
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter());
|
||||||
|
|
||||||
if (offset.HasValue)
|
if (offset.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -824,6 +832,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 +895,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 +917,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 +969,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 +983,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 +991,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 +1164,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)
|
||||||
{
|
{
|
||||||
|
@ -1158,7 +1207,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate concat configuration entries for each file and write to file
|
// Generate concat configuration entries for each file and write to file
|
||||||
using StreamWriter sw = new StreamWriter(concatFilePath);
|
Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
|
||||||
|
using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
|
||||||
foreach (var path in files)
|
foreach (var path in files)
|
||||||
{
|
{
|
||||||
var mediaInfoResult = GetMediaInfo(
|
var mediaInfoResult = GetMediaInfo(
|
||||||
|
@ -1177,7 +1227,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
||||||
|
|
||||||
// Add file path stanza to concat configuration
|
// Add file path stanza to concat configuration
|
||||||
sw.WriteLine("file '{0}'", path);
|
sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
|
||||||
|
|
||||||
// Add duration stanza to concat configuration
|
// Add duration stanza to concat configuration
|
||||||
sw.WriteLine("duration {0}", duration);
|
sw.WriteLine("duration {0}", duration);
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -717,6 +721,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||||
if (streamInfo.CodecType == CodecType.Audio)
|
if (streamInfo.CodecType == CodecType.Audio)
|
||||||
{
|
{
|
||||||
stream.Type = MediaStreamType.Audio;
|
stream.Type = MediaStreamType.Audio;
|
||||||
|
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||||
|
stream.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||||
|
|
||||||
stream.Channels = streamInfo.Channels;
|
stream.Channels = streamInfo.Channels;
|
||||||
|
|
||||||
|
@ -779,11 +785,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -501,11 +501,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
List<MediaStream> subtitleStreams,
|
List<MediaStream> subtitleStreams,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var inputPath = mediaSource.Path;
|
var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
|
||||||
var outputPaths = new List<string>();
|
var outputPaths = new List<string>();
|
||||||
var args = string.Format(
|
var args = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"-i \"{0}\" -copyts",
|
"-i {0} -copyts",
|
||||||
inputPath);
|
inputPath);
|
||||||
|
|
||||||
foreach (var subtitleStream in subtitleStreams)
|
foreach (var subtitleStream in subtitleStreams)
|
||||||
|
@ -676,7 +676,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
|
|
||||||
var processArgs = string.Format(
|
var processArgs = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||||
inputPath,
|
inputPath,
|
||||||
subtitleStreamIndex,
|
subtitleStreamIndex,
|
||||||
outputCodec,
|
outputCodec,
|
||||||
|
|
|
@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||||
o.PoolInitialFill = 1;
|
o.PoolInitialFill = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -559,7 +561,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||||
|
|
||||||
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
||||||
{
|
{
|
||||||
if (EnableThrottling(state))
|
if (EnableThrottling(state)
|
||||||
|
&& (_mediaEncoder.IsPkeyPauseSupported
|
||||||
|
|| _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
|
||||||
{
|
{
|
||||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||||
transcodingJob.TranscodingThrottler.Start();
|
transcodingJob.TranscodingThrottler.Start();
|
||||||
|
|
|
@ -858,7 +858,7 @@ namespace MediaBrowser.Model.Dlna
|
||||||
{
|
{
|
||||||
audioStream = directAudioStream;
|
audioStream = directAudioStream;
|
||||||
playlistItem.AudioStreamIndex = audioStream.Index;
|
playlistItem.AudioStreamIndex = audioStream.Index;
|
||||||
playlistItem.AudioCodecs = new[] { audioStream.Codec };
|
playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec };
|
||||||
|
|
||||||
// Copy matching audio codec options
|
// Copy matching audio codec options
|
||||||
playlistItem.AudioSampleRate = audioStream.SampleRate;
|
playlistItem.AudioSampleRate = audioStream.SampleRate;
|
||||||
|
@ -897,18 +897,18 @@ namespace MediaBrowser.Model.Dlna
|
||||||
|
|
||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
i.ContainsAnyCodec(videoCodecs, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
|
||||||
var isFirstAppliedCodecProfile = true;
|
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
||||||
|
.Reverse();
|
||||||
|
|
||||||
foreach (var i in appliedVideoConditions)
|
foreach (var i in appliedVideoConditions)
|
||||||
{
|
{
|
||||||
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
foreach (var transcodingVideoCodec in videoCodecs)
|
||||||
foreach (var transcodingVideoCodec in transcodingVideoCodecs)
|
|
||||||
{
|
{
|
||||||
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
||||||
{
|
{
|
||||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
|
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
|
||||||
isFirstAppliedCodecProfile = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -929,18 +929,18 @@ namespace MediaBrowser.Model.Dlna
|
||||||
|
|
||||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||||
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
i.ContainsAnyCodec(audioCodecs, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
|
||||||
isFirstAppliedCodecProfile = true;
|
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
||||||
|
.Reverse();
|
||||||
|
|
||||||
foreach (var codecProfile in appliedAudioConditions)
|
foreach (var codecProfile in appliedAudioConditions)
|
||||||
{
|
{
|
||||||
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
foreach (var transcodingAudioCodec in audioCodecs)
|
||||||
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
|
|
||||||
{
|
{
|
||||||
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
||||||
{
|
{
|
||||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
|
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
|
||||||
isFirstAppliedCodecProfile = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("dvdsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sub", 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.11</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,10 +100,19 @@ namespace MediaBrowser.Providers.Manager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start by validating images
|
var localImagesFailed = false;
|
||||||
|
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
||||||
|
|
||||||
|
// Only validate already registered images if we are replacing and saving locally
|
||||||
|
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
|
||||||
|
{
|
||||||
|
item.ValidateImages();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Run full image validation and register new local images
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Always validate images and check for new locally stored ones.
|
|
||||||
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
||||||
{
|
{
|
||||||
updateType |= ItemUpdateType.ImageUpdate;
|
updateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
@ -119,10 +123,12 @@ namespace MediaBrowser.Providers.Manager
|
||||||
localImagesFailed = true;
|
localImagesFailed = true;
|
||||||
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
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,26 +664,25 @@ 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>();
|
||||||
|
|
||||||
|
// Do not execute local providers if we are identifying or replacing with local metadata saving enabled
|
||||||
|
if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
|
||||||
|
{
|
||||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var providerName = provider.GetType().Name;
|
||||||
|
@ -720,15 +728,9 @@ namespace MediaBrowser.Providers.Manager
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
// Only one local provider allowed per item
|
|
||||||
if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
|
|
||||||
{
|
|
||||||
hasLocalMetadata = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -746,11 +748,12 @@ namespace MediaBrowser.Providers.Manager
|
||||||
refreshResult.ErrorMessage = ex.Message;
|
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;
|
||||||
|
@ -948,13 +939,9 @@ namespace MediaBrowser.Providers.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
|
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
|
||||||
{
|
|
||||||
// Safeguard against incoming data having an empty name
|
|
||||||
if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
|
|
||||||
{
|
{
|
||||||
target.OriginalTitle = 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
|
||||||
|
{
|
||||||
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
|
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
|
||||||
|
{
|
||||||
target.PreferredMetadataLanguage = source.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,30 +1193,18 @@ namespace MediaBrowser.Providers.Manager
|
||||||
{
|
{
|
||||||
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
|
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
|
||||||
}
|
}
|
||||||
}
|
else if (sourceHasAlbumArtist.AlbumArtists.Count >= 0)
|
||||||
}
|
|
||||||
|
|
||||||
private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
|
|
||||||
{
|
{
|
||||||
if (replaceData || !target.CriticRating.HasValue)
|
targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray();
|
||||||
{
|
|
||||||
target.CriticRating = source.CriticRating;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
|
|
||||||
{
|
|
||||||
if (replaceData || target.RemoteTrailers.Count == 0)
|
|
||||||
{
|
|
||||||
target.RemoteTrailers = source.RemoteTrailers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
|
private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user