diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index c6670e9f58..af52642792 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.3",
+ "version": "8.0.7",
"commands": [
"dotnet-ef"
]
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index b690b82c24..31ae502634 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -1,58 +1,131 @@
name: Issue Report
description: File an issue report
-title: "[Issue]: "
labels: [bug, triage]
body:
- type: markdown
+ id: introduction
attributes:
value: |
- Thanks for taking the time to report an issue. Before submitting a report, please do the following:
- 1. Please head to our forum or chat rooms and troubleshoot with volunteers if you haven't already. Links can be found here: https://jellyfin.org/contact/
- 2. Please search the bug tracker for similar issues. If you do find one, please comment there instead of opening a new bug report.
- 3. If you decide to open a new report, please provide as much detail as possible.
- 4. Please **ONLY** report **ONE** issue per report. If you are experiencing multiple issues, please open multiple reports.
+ ### Thank you for taking the time to report an issue!
+ Please keep in mind that Jellyfin is a [free and open-source](https://jellyfin.org/docs/general/about) project, made up entirely and exclusively of **volunteers** who donate their free time to the project.
+ - type: checkboxes
+ id: before-posting
+ attributes:
+ label: "This issue respects the following points:"
+ description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
+ options:
+ - label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/).
+ required: true
+ - label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
+ required: true
+ - label: I'm using an up to date version of Jellyfin Server stable, unstable or master; We generally do not support previous older versions. If possible, please update to the latest version before opening an issue.
+ required: true
+ - label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
+ required: true
+ - label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
+ required: true
+ - type: markdown
+ id: preliminary-information
+ attributes:
+ value: |
+ ### General preliminary information
+
+ Please keep the following in mind when creating this issue:
+
+ 1. Fill in as much of the template as possible. When you are unsure about the relevancy of a section, do include the information requested in that section. Only leave out information in sections when you are completely sure about it not being relevant.
+ 2. Provide as much detail as possible. Do not assume other people to know what is going on.
+ 3. Keep everything readable and structured. Nobody enjoys reading poorly written reports that are difficult to understand.
+ 4. Keep an eye on your report as long as it is open, your involvement might be requested at a later moment.
+ 5. Keep the title short and descriptive. The title is not the place to write down a full description of the issue.
+ 6. When deciding to leave out information in a field, leave it blank and empty. Avoid writing things such as `n/a` for empty fields.
- type: textarea
- id: what-happened
+ id: bug-description
attributes:
- label: Please describe your bug
- description: Also tell us, what did you expect to happen?
+ label: Description of the bug
+ description: Please provide a detailed description on the bug you encountered, in a readable and comprehensible way.
placeholder: |
- The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
- If you are using an old release of Jellyfin, please also explain why.
+ After upgrading to version x.y.z of Jellyfin, the "login disclaimer" is showing incorrect text. It appears to me that it is appending the server name to the end of the login disclaimer, and showing that to a user. It might be a regression from pull request x. I have tried rebooting my host as well as my container multiple times. I tested this functionality on different clients, and it happens to all the tested clients (client x, y, z), that support the login disclaimer functionality. This makes me believe it is a server side issue.
validations:
required: true
- type: textarea
id: repro-steps
attributes:
- label: Reproduction Steps
+ label: Reproduction steps
+ description: Reproduction steps should be complete and self-contained. Anyone can reproduce this issue by following these steps. Furthermore, the steps should be clear and easy to follow.
+ placeholder: |
+ 1. Sign in on the Jellyfin web client, with an admin account, using a browser of your choice.
+ 2. Navigate to the dashboard.
+ 3. Select "general".
+ 4. Change the login disclaimer to something like "I am a cool disclaimer!"
+ 5. Save the settings.
+ 6. Sign out.
+ 7. Make sure you are on the sign in screen. Otherwise, navigate to the sign in screen manually.
+ validations:
+ required: true
+ - type: textarea
+ id: actual-behavior
+ attributes:
+ label: What is the current _bug_ behavior?
+ description: Write down the incorrect behavior that currently happens after following the reproduction steps.
+ placeholder: |
+ The login disclaimer on the sign in screen has the server name appended to the text. The text shown is: "I am a cool disclaimer!jellyfinserver".
+ validations:
+ required: true
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label: What is the expected _correct_ behavior?
+ description: Write down the correct expected behavior that is supposed to happen after following the reproduction steps.
placeholder: |
- 1. In this environment...
- 2. With this config...
- 3. Run '...'
- 4. See error...
+ The login disclaimer on the sign in screen should only show the configured text. The text that should be shown is: "I am a cool disclaimer!".
validations:
required: true
- type: dropdown
id: version
attributes:
- label: Jellyfin Version
- description: What version of Jellyfin are you running?
+ label: Jellyfin Server version
+ description: What version of Jellyfin are you using?
options:
- - 10.8.13
- - 10.8.12
- - 10.8.11 or older (please specify)
- - Unstable (master branch)
+ - 10.9.7
+ - Master
+ - Unstable
+ - Older*
validations:
required: true
- type: input
- id: version-other
+ id: version-master
+ attributes:
+ label: "Specify commit id"
+ description: Fill in this field in case the option 'master' is selected. Provide the commit id it was built on.
+ placeholder: |
+ 610e56baafc3011e1bfa043bdabb567bda0c2ab0
+ - type: input
+ id: version-unstable
+ attributes:
+ label: "Specify unstable release number"
+ description: Fill in this field in case the option 'unstable' is selected. Provide the unstable release number.
+ placeholder: |
+ 2024050906
+ - type: input
+ id: version-older
attributes:
- label: "if other:"
- placeholder: Other
+ label: "Specify version number"
+ description: Fill in this field in case the option 'older' is selected. Provide the version number.
+ placeholder: |
+ x.y.z
+ - type: input
+ id: build-version
+ attributes:
+ label: "Specify the build version"
+ description: Please provide the build version that is shown in the dashboard.
+ validations:
+ required: true
- type: textarea
+ id: environment-information
attributes:
label: Environment
description: |
+ Accurately fill in as much environment details as possible. If a certain environment field is not shown in the template below, but you consider useful information, please include it.
Examples:
- **OS**: [e.g. Debian 11, Windows 10]
- **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
@@ -87,21 +160,22 @@ body:
validations:
required: true
- type: markdown
+ id: general-information-logs
attributes:
value: |
- When providing logs, please keep the following things in mind.
- 1. **DO NOT** use external paste services.
+ When providing logs, please keep the following things in mind:
+ 1. **DO NOT** use external paste services. If logs are too large to paste into the field, upload them as text files.
2. Please provide complete logs.
- - For server logs, include everything you think is important plus *10 lines before and after*
+ - For server logs, ensure to capture all relevant information, encompassing both the events leading up to and following the occurrence of the issue. Typically, providing 10 *lines preceding and succeeding* the problem should be adequate.
- For ffmpeg logs, please provide the entire file unmodified.
- 3. Please do not run logs through any translation program. Especially beware if your browser translates pages by default.
+ 3. Please do not run logs through any translation program. We exclusively accept raw, untranslated logs. Particularly exercise caution if your browser automatically translates pages by default.
+ - Do not forget to censor out personal information such as public IP addresses.
4. Please do not include logs as screenshots, with the only exception being client logs in browsers.
- type: textarea
- id: logs
+ id: jellyfin-logs
attributes:
label: Jellyfin logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
- placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
render: shell
validations:
required: true
@@ -109,24 +183,20 @@ body:
id: ffmpeg-logs
attributes:
label: FFmpeg logs
- description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
- placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+ description: Relevant FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log. This field is considered mandatory for transcoding related issues. It's also important to include the specific codec details.
render: shell
- type: textarea
- id: browserlogs
+ id: browser-logs
attributes:
- label: Please attach any browser or client logs here
- placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
+ label: Client / Browser logs
+ description: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
- type: textarea
id: screenshots
attributes:
- label: Please attach any screenshots here
- placeholder: Images can be pasted directly into the textbox and will be hosted by github.
- - type: checkboxes
- id: terms
+ label: Relevant screenshots or videos
+ description: Attach relevant screenshots or videos related to this report.
+ - type: textarea
+ id: additional-information
attributes:
- label: Code of Conduct
- description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
- options:
- - label: I agree to follow this project's Code of Conduct
- required: true
+ label: Additional information
+ description: Any additional information that might be useful to this issue.
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 7b76e47f56..c6ea1d7ca9 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup .NET
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
+ uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
with:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+ uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+ uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
+ uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 0864299f79..54a0615567 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -3,6 +3,8 @@ on:
push:
branches:
- master
+ tags:
+ - 'v*'
pull_request_target:
permissions: {}
@@ -14,18 +16,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
+ uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
with:
dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: openapi-head
retention-days: 14
@@ -39,7 +41,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -53,13 +55,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
+ uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
with:
dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: openapi-base
retention-days: 14
@@ -78,12 +80,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: openapi-base
path: openapi-base
@@ -99,13 +101,26 @@ jobs:
- id: read-diff
name: Read openapi-diff output
run: |
+ # Read and fix markdown
body=$(cat openapi-changes.md)
- body="${body//'%'/'%25'}"
- body="${body//$'\n'/'%0A'}"
- body="${body//$'\r'/'%0D'}"
- echo ::set-output name=body::$body
+ # Write to workflow summary
+ echo "$body" >> $GITHUB_STEP_SUMMARY
+ # Set ApiChanged var
+ if [ "$body" != '' ]; then
+ echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
+ else
+ echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
+ fi
+ # Add header/footer for diff comment
+ echo '' > openapi-changes-reply.md
+ echo "" >> openapi-changes-reply.md
+ echo "Changes in OpenAPI specification found. Expand to see details.
" >> openapi-changes-reply.md
+ echo "" >> openapi-changes-reply.md
+ echo "$body" >> openapi-changes-reply.md
+ echo "" >> openapi-changes-reply.md
+ echo " " >> openapi-changes-reply.md
- name: Find difference comment
- uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.0
+ uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -113,22 +128,15 @@ jobs:
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
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:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
- body: |
-
-
- Changes in OpenAPI specification found. Expand to see details.
-
- ${{ steps.read-diff.outputs.body }}
-
-
+ body-path: openapi-changes-reply.md
- name: Edit difference comment (unchanged)
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:
issue-number: ${{ github.event.pull_request.number }}
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.
- publish:
+ publish-unstable:
name: OpenAPI - Publish Unstable Spec
- if: |
- github.event_name != 'pull_request_target' &&
- contains(github.repository_owner, 'jellyfin')
+ if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
@@ -152,7 +158,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: openapi-head
path: openapi-head
@@ -174,6 +180,12 @@ jobs:
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}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
@@ -187,10 +199,73 @@ jobs:
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
- # Create new jellyfin-openapi-stable.json symlink
+ # Create new jellyfin-openapi-unstable.json symlink
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
- # Check that the previous openapi spec is correct
+ # Check that the previous openapi unstable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
+ ) 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@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ 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
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 612e8c7510..91c2be87b3 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -19,9 +19,9 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- - uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
+ - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@3e39bd1b454c2bac14560547e4394f9317672705 # 5.2.4
+ uses: danielpalme/ReportGenerator-GitHub-Action@5808021ec4deecb0ab3da051d49b4ce65fcc20af # 5.3.8
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index d78f11166c..b791858553 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -128,11 +128,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
+ uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index e53234641b..6172455c2e 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -10,11 +10,11 @@ jobs:
issues: write
steps:
- name: pull in script
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
+ uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index 05517bb030..5d342b7f84 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
+ uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml
index e0383afd23..575f2d7562 100644
--- a/.github/workflows/release-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 8550222a16..edbc846d63 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -183,6 +183,9 @@
- [btopherjohnson](https://github.com/btopherjohnson)
- [GeorgeH005](https://github.com/GeorgeH005)
- [Vedant](https://github.com/viktory36/)
+ - [NotSaifA](https://github.com/NotSaifA)
+ - [HonestlyWhoKnows](https://github.com/honestlywhoknows)
+ - [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
# Emby Contributors
@@ -255,3 +258,4 @@
- [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner)
+ - [Nathan McCrina](https://github.com/nfmccrina)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ac0a523c58..825301bfcb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
-
+
@@ -13,43 +13,42 @@
-
+
-
+
-
+
-
+
-
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
+
+
-
+
@@ -60,33 +59,33 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
-
+
+
-
+
-
+
\ No newline at end of file
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 4bd226d95e..333d237a24 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -537,6 +537,12 @@ namespace Emby.Naming.Common
"extras",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.Unknown,
+ ExtraRuleType.DirectoryName,
+ "extra",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 97015efd0b..7eb131575d 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -36,7 +36,7 @@
Jellyfin Contributors
Jellyfin.Naming
- 10.9.0
+ 10.10.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 9d54533c24..7a01b02f3c 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
pathInfo.Language = culture.ThreeLetterISOLanguageName;
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;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Naming/TV/TvParserHelpers.cs b/Emby.Naming/TV/TvParserHelpers.cs
new file mode 100644
index 0000000000..0299178582
--- /dev/null
+++ b/Emby.Naming/TV/TvParserHelpers.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Linq;
+using MediaBrowser.Model.Entities;
+
+namespace Emby.Naming.TV;
+
+///
+/// Helper class for TV metadata parsing.
+///
+public static class TvParserHelpers
+{
+ private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
+ private static readonly string[] _endedState = ["Cancelled", "Canceled"];
+
+ ///
+ /// Tries to parse a string into .
+ ///
+ /// The status string.
+ /// The .
+ /// Returns true if parsing was successful.
+ 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;
+ }
+}
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 39524be1d4..dc845b2d7e 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -104,6 +104,6 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the temp directory within the cache folder.
///
/// The temp directory.
- public string TempDirectory => Path.Combine(CachePath, "temp");
+ public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
}
}
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index a2f38c8c2d..9e98d5ce09 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -127,15 +127,11 @@ namespace Emby.Server.Implementations.AppBase
if (_configurationFactories is null)
{
- _configurationFactories = new[] { factory };
+ _configurationFactories = [factory];
}
else
{
- var oldLen = _configurationFactories.Length;
- var arr = new IConfigurationFactory[oldLen + 1];
- _configurationFactories.CopyTo(arr, 0);
- arr[oldLen] = factory;
- _configurationFactories = arr;
+ _configurationFactories = [.._configurationFactories, factory];
}
_configurationStores = _configurationFactories
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index acabbb059b..5bf9c4fc29 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -109,13 +109,13 @@ namespace Emby.Server.Implementations
///
/// The disposable parts.
///
- private readonly ConcurrentDictionary _disposableParts = new();
+ private readonly ConcurrentBag _disposableParts = new();
private readonly DeviceId _deviceId;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
- private readonly IPluginManager _pluginManager;
+ private readonly PluginManager _pluginManager;
private List _creatingInstances;
@@ -161,7 +161,7 @@ namespace Emby.Server.Implementations
ApplicationPaths.PluginsPath,
ApplicationVersion);
- _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
+ _disposableParts.Add(_pluginManager);
}
///
@@ -360,7 +360,7 @@ namespace Emby.Server.Implementations
{
foreach (var part in parts.OfType())
{
- _disposableParts.TryAdd(part, byte.MinValue);
+ _disposableParts.Add(part);
}
}
@@ -381,7 +381,7 @@ namespace Emby.Server.Implementations
{
foreach (var part in parts.OfType())
{
- _disposableParts.TryAdd(part, byte.MinValue);
+ _disposableParts.Add(part);
}
}
@@ -422,7 +422,7 @@ namespace Emby.Server.Implementations
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
{
- DotNetRuntimeStatsBuilder.Default().StartCollecting();
+ _disposableParts.Add(DotNetRuntimeStatsBuilder.Default().StartCollecting());
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@@ -457,7 +457,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(ConfigurationManager);
serviceCollection.AddSingleton(ConfigurationManager);
serviceCollection.AddSingleton(this);
- serviceCollection.AddSingleton(_pluginManager);
+ serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton(ApplicationPaths);
serviceCollection.AddSingleton();
@@ -664,7 +664,8 @@ namespace Emby.Server.Implementations
GetExports(),
GetExports(),
GetExports(),
- GetExports());
+ GetExports(),
+ GetExports());
Resolve().AddParts(GetExports());
}
@@ -965,7 +966,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name);
- foreach (var (part, _) in _disposableParts)
+ foreach (var part in _disposableParts.ToArray())
{
var partType = part.GetType();
if (partType == type)
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index b34d0f21ef..e414792ba0 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections");
- await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
+ await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First();
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index f0c2676279..c06cd85109 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -19,7 +19,8 @@ namespace Emby.Server.Implementations
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
- { SqliteCacheSizeKey, "20000" }
+ { SqliteCacheSizeKey, "20000" },
+ { SqliteDisableSecondLevelCacheKey, bool.FalseString }
};
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index bf079d90ca..5291999dc0 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
public abstract class BaseSqliteRepository : IDisposable
{
private bool _disposed = false;
+ private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+ private SqliteConnection _writeConnection;
///
/// Initializes a new instance of the class.
@@ -28,17 +31,6 @@ namespace Emby.Server.Implementations.Data
///
protected string DbFilePath { get; set; }
- ///
- /// Gets or sets the number of write connections to create.
- ///
- /// Path to the DB file.
- protected int WriteConnectionsCount { get; set; } = 1;
-
- ///
- /// Gets or sets the number of read connections to create.
- ///
- protected int ReadConnectionsCount { get; set; } = 1;
-
///
/// Gets the logger.
///
@@ -98,9 +90,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();
if (CacheSize.HasValue)
@@ -135,17 +173,17 @@ namespace Emby.Server.Implementations.Data
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();
command.CommandText = sql;
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");
foreach (var row in statement.ExecuteQuery())
@@ -159,7 +197,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
- protected List GetColumnNames(SqliteConnection connection, string table)
+ protected List GetColumnNames(ManagedConnection connection, string table)
{
var columnNames = new List();
@@ -174,7 +212,7 @@ namespace Emby.Server.Implementations.Data
return columnNames;
}
- protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List existingColumnNames)
+ protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
@@ -186,10 +224,7 @@ namespace Emby.Server.Implementations.Data
protected void CheckDisposed()
{
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
- }
+ ObjectDisposedException.ThrowIf(_disposed, this);
}
///
@@ -210,6 +245,24 @@ namespace Emby.Server.Implementations.Data
return;
}
+ if (dispose)
+ {
+ _writeLock.Wait();
+ try
+ {
+ _writeConnection.Dispose();
+ }
+ finally
+ {
+ _writeLock.Release();
+ }
+
+ _writeLock.Dispose();
+ }
+
+ _writeConnection = null;
+ _writeLock = null;
+
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
new file mode 100644
index 0000000000..860950b303
--- /dev/null
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -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 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;
+ }
+}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index f1e60915d0..60f5ee47ac 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
private const string SaveItemCommandText =
@"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
+ (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
@@ -111,6 +111,7 @@ namespace Emby.Server.Implementations.Data
"DateLastMediaAdded",
"Album",
"LUFS",
+ "NormalizationGain",
"CriticRating",
"IsVirtualItem",
"SeriesName",
@@ -327,7 +328,6 @@ namespace Emby.Server.Implementations.Data
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
CacheSize = configuration.GetSqliteCacheSize();
- ReadConnectionsCount = Environment.ProcessorCount * 2;
}
///
@@ -479,6 +479,7 @@ namespace Emby.Server.Implementations.Data
AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@@ -602,7 +603,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit();
}
- private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples)
+ private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples)
{
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@@ -889,6 +890,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@Album", item.Album);
saveItemStatement.TryBind("@LUFS", item.LUFS);
+ saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
if (item is IHasSeries hasSeriesName)
@@ -1047,9 +1049,10 @@ namespace Emby.Server.Implementations.Data
foreach (var part in value.SpanSplit('|'))
{
var providerDelimiterIndex = part.IndexOf('=');
- if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('='))
+ // Don't let empty values through
+ if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1)
{
- item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString());
+ item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString());
}
}
}
@@ -1261,7 +1264,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1298,16 +1301,15 @@ namespace Emby.Server.Implementations.Data
&& type != typeof(Book)
&& type != typeof(LiveTvProgram)
&& type != typeof(AudioBook)
- && type != typeof(Audio)
&& type != typeof(MusicAlbum);
}
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);
@@ -1320,7 +1322,7 @@ namespace Emby.Server.Implementations.Data
BaseItem item = null;
- if (TypeRequiresDeserialization(type))
+ if (TypeRequiresDeserialization(type) && !skipDeserialization)
{
try
{
@@ -1675,6 +1677,11 @@ namespace Emby.Server.Implementations.Data
item.LUFS = lUFS;
}
+ if (reader.TryGetSingle(index++, out var normalizationGain))
+ {
+ item.NormalizationGain = normalizationGain;
+ }
+
if (reader.TryGetSingle(index++, out var criticRating))
{
item.CriticRating = criticRating;
@@ -1883,7 +1890,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
var chapters = new List();
- 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"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1902,7 +1909,7 @@ namespace Emby.Server.Implementations.Data
{
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"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1976,7 +1983,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit();
}
- private void InsertChapters(Guid idBlob, IReadOnlyList chapters, SqliteConnection db)
+ private void InsertChapters(Guid idBlob, IReadOnlyList chapters, ManagedConnection db)
{
var startIndex = 0;
var limit = 100;
@@ -2318,14 +2325,7 @@ namespace Emby.Server.Implementations.Data
columns.Add(builder.ToString());
- var oldLen = query.ExcludeItemIds.Length;
- var newLen = oldLen + item.ExtraIds.Length + 1;
- var excludeIds = new Guid[newLen];
- query.ExcludeItemIds.CopyTo(excludeIds, 0);
- excludeIds[oldLen] = item.Id;
- item.ExtraIds.CopyTo(excludeIds, oldLen + 1);
-
- query.ExcludeItemIds = excludeIds;
+ query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds];
query.ExcludeProviderIds = item.ProviderIds;
}
@@ -2472,7 +2472,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2540,7 +2540,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var items = new List();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2564,7 +2564,7 @@ namespace Emby.Server.Implementations.Data
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)
{
items.Add(item);
@@ -2748,7 +2748,7 @@ namespace Emby.Server.Implementations.Data
var list = new List();
var result = new QueryResult();
- using var connection = GetConnection();
+ using var connection = GetConnection(true);
using var transaction = connection.BeginTransaction();
if (!isReturningZeroItems)
{
@@ -2776,7 +2776,7 @@ namespace Emby.Server.Implementations.Data
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)
{
list.Add(item);
@@ -2833,10 +2833,7 @@ namespace Emby.Server.Implementations.Data
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
}
- var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
- prepend.CopyTo(arr, 0);
- orderBy.CopyTo(arr, prepend.Count);
- orderBy = query.OrderBy = arr;
+ orderBy = query.OrderBy = [.. prepend, .. orderBy];
}
else if (orderBy.Count == 0)
{
@@ -2933,7 +2930,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var list = new List();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -4197,7 +4194,19 @@ namespace Emby.Server.Implementations.Data
{
int index = 0;
string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
+ // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
+ // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
+ if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
+ {
+ whereClauses.Add($"""
+ ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
+ OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
+ """);
+ }
+ else
+ {
+ whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
+ }
}
else
{
@@ -4470,7 +4479,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
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))
{
@@ -4503,7 +4512,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4541,7 +4550,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4626,7 +4635,7 @@ AND Type = @InternalPersonType)");
return whereClauses;
}
- private void UpdateAncestors(Guid itemId, List ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
+ private void UpdateAncestors(Guid itemId, List ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
{
if (itemId.IsEmpty())
{
@@ -4781,7 +4790,7 @@ AND Type = @InternalPersonType)");
var list = new List();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
foreach (var row in statement.ExecuteQuery())
@@ -4981,8 +4990,8 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction(deferred: true))
+ using (var connection = GetConnection(true))
+ using (var transaction = connection.BeginTransaction())
{
if (!isReturningZeroItems)
{
@@ -5014,7 +5023,7 @@ AND Type = @InternalPersonType)");
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)
{
var countStartColumn = columns.Count - 1;
@@ -5137,12 +5146,12 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i)));
// Remove all invalid values.
- list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
+ list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
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())
{
@@ -5161,7 +5170,7 @@ AND Type = @InternalPersonType)");
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;
var startIndex = 0;
@@ -5195,12 +5204,6 @@ AND Type = @InternalPersonType)");
var itemValue = currentValueInfo.Value;
- // Don't save if invalid
- if (string.IsNullOrWhiteSpace(itemValue))
- {
- continue;
- }
-
statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
statement.TryBind("@Value" + index, itemValue);
statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
@@ -5221,24 +5224,25 @@ AND Type = @InternalPersonType)");
throw new ArgumentNullException(nameof(itemId));
}
- ArgumentNullException.ThrowIfNull(people);
-
CheckDisposed();
using var connection = GetConnection();
using var transaction = connection.BeginTransaction();
- // First delete chapters
+ // Delete all existing people first
using var command = connection.CreateCommand();
command.CommandText = "delete from People where ItemId=@ItemId";
command.TryBind("@ItemId", itemId);
command.ExecuteNonQuery();
- InsertPeople(itemId, people, connection);
+ if (people is not null)
+ {
+ InsertPeople(itemId, people, connection);
+ }
transaction.Commit();
}
- private void InsertPeople(Guid id, List people, SqliteConnection db)
+ private void InsertPeople(Guid id, List people, ManagedConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5334,7 +5338,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC";
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
{
var list = new List();
@@ -5387,7 +5391,7 @@ AND Type = @InternalPersonType)");
transaction.Commit();
}
- private void InsertMediaStreams(Guid id, IReadOnlyList streams, SqliteConnection db)
+ private void InsertMediaStreams(Guid id, IReadOnlyList streams, ManagedConnection db)
{
const int Limit = 10;
var startIndex = 0;
@@ -5700,13 +5704,17 @@ AND Type = @InternalPersonType)");
item.Rotation = rotation;
}
- if (item.Type == MediaStreamType.Subtitle)
+ if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{
- item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
item.LocalizedDefault = _localization.GetLocalizedString("Default");
- item.LocalizedForced = _localization.GetLocalizedString("Forced");
item.LocalizedExternal = _localization.GetLocalizedString("External");
- item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+
+ if (item.Type is MediaStreamType.Subtitle)
+ {
+ item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+ item.LocalizedForced = _localization.GetLocalizedString("Forced");
+ item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+ }
}
return item;
@@ -5728,7 +5736,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC";
var list = new List();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, cmdText))
{
statement.TryBind("@ItemId", query.ItemId);
@@ -5778,7 +5786,7 @@ AND Type = @InternalPersonType)");
private void InsertMediaAttachments(
Guid id,
IReadOnlyList attachments,
- SqliteConnection db,
+ ManagedConnection db,
CancellationToken cancellationToken)
{
const int InsertAtOnce = 10;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index a5edcc58c0..634eaf85ef 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -58,7 +58,8 @@ namespace Emby.Server.Implementations.Data
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
+ "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
+ "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
if (!userDataTableExists)
{
@@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private void ImportUserIds(SqliteConnection db, IEnumerable users)
+ private void ImportUserIds(ManagedConnection db, IEnumerable users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private List GetAllUserIdsWithUserData(SqliteConnection db)
+ private List GetAllUserIdsWithUserData(ManagedConnection db)
{
var list = new List();
@@ -175,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)"))
{
@@ -266,7 +267,7 @@ namespace Emby.Server.Implementations.Data
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"))
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 7812687ea3..19902b26a0 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -668,12 +668,13 @@ namespace Emby.Server.Implementations.Dto
{
dto.ImageBlurHashes ??= new Dictionary>();
- if (!dto.ImageBlurHashes.ContainsKey(image.Type))
+ if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{
- dto.ImageBlurHashes[image.Type] = new Dictionary();
+ value = new Dictionary();
+ dto.ImageBlurHashes[image.Type] = value;
}
- dto.ImageBlurHashes[image.Type][tag] = image.BlurHash;
+ value[tag] = image.BlurHash;
}
return tag;
@@ -897,16 +898,21 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
- dto.LUFS = item.LUFS;
+ if (item.LUFS.HasValue)
+ {
+ // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
+ dto.NormalizationGain = -18f - item.LUFS;
+ }
+ else if (item.NormalizationGain.HasValue)
+ {
+ dto.NormalizationGain = item.NormalizationGain;
+ }
// Add audio info
if (item is Audio audio)
{
dto.Album = audio.Album;
- if (audio.ExtraType.HasValue)
- {
- dto.ExtraType = audio.ExtraType.Value.ToString();
- }
+ dto.ExtraType = audio.ExtraType;
var albumParent = audio.AlbumEntity;
@@ -1058,10 +1064,7 @@ namespace Emby.Server.Implementations.Dto
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
}
- if (video.ExtraType.HasValue)
- {
- dto.ExtraType = video.ExtraType.Value.ToString();
- }
+ dto.ExtraType = video.ExtraType;
}
if (options.ContainsField(ItemFields.MediaStreams))
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index f83da566b2..cb6f7e1d35 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -101,14 +101,14 @@ namespace Emby.Server.Implementations.HttpServer
var pipe = new Pipe();
var writer = pipe.Writer;
- ValueWebSocketReceiveResult receiveresult;
+ ValueWebSocketReceiveResult receiveResult;
do
{
// Allocate at least 512 bytes from the PipeWriter
Memory memory = writer.GetMemory(512);
try
{
- receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
+ receiveResult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
}
catch (WebSocketException ex)
{
@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.HttpServer
break;
}
- int bytesRead = receiveresult.Count;
+ int bytesRead = receiveResult.Count;
if (bytesRead == 0)
{
break;
@@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.HttpServer
LastActivityDate = DateTime.UtcNow;
- if (receiveresult.EndOfMessage)
+ if (receiveResult.EndOfMessage)
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
}
while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
- && receiveresult.MessageType != WebSocketMessageType.Close);
+ && receiveResult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
@@ -199,13 +199,20 @@ namespace Emby.Server.Implementations.HttpServer
}
else
{
- await OnReceive(
- new WebSocketMessageInfo
- {
- MessageType = stub.MessageType,
- Data = stub.Data?.ToString(), // Data can be null
- Connection = this
- }).ConfigureAwait(false);
+ try
+ {
+ await OnReceive(
+ new WebSocketMessageInfo
+ {
+ MessageType = stub.MessageType,
+ Data = stub.Data?.ToString(), // Data can be null
+ Connection = this
+ }).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogWarning(exception, "Failed to process WebSocket message");
+ }
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 52f14b0b10..774d3563cb 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
- using var connection = new WebSocketConnection(
+ var connection = new WebSocketConnection(
_loggerFactory.CreateLogger(),
webSocket,
authorizationInfo,
@@ -56,17 +56,19 @@ namespace Emby.Server.Implementations.HttpServer
{
OnReceive = ProcessWebSocketMessageReceived
};
-
- var tasks = new Task[_webSocketListeners.Length];
- for (var i = 0; i < _webSocketListeners.Length; ++i)
+ await using (connection.ConfigureAwait(false))
{
- tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
- }
+ var tasks = new Task[_webSocketListeners.Length];
+ for (var i = 0; i < _webSocketListeners.Length; ++i)
+ {
+ tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
+ }
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ await Task.WhenAll(tasks).ConfigureAwait(false);
- await connection.ReceiveAsync().ConfigureAwait(false);
- _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+ await connection.ReceiveAsync().ConfigureAwait(false);
+ _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+ }
}
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
{
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 67854a2a7a..28bb29df85 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -80,12 +80,14 @@ namespace Emby.Server.Implementations.IO
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
// path is actually a stream
- if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal))
+ if (string.IsNullOrWhiteSpace(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
return filePath;
@@ -97,17 +99,10 @@ namespace Emby.Server.Implementations.IO
return filePath;
}
- var firstChar = filePath[0];
- if (firstChar == '/')
- {
- // for this we don't really know
- return filePath;
- }
-
var filePathSpan = filePath.AsSpan();
- // relative path
- if (firstChar == '\\')
+ // relative path on windows
+ if (filePath[0] == '\\')
{
filePathSpan = filePathSpan.Slice(1);
}
@@ -394,7 +389,7 @@ namespace Emby.Server.Implementations.IO
var info = new FileInfo(path);
if (info.Exists &&
- ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
+ (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
{
if (isHidden)
{
@@ -422,8 +417,8 @@ namespace Emby.Server.Implementations.IO
return;
}
- if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
- && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
+ if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
+ && (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
{
return;
}
@@ -471,7 +466,7 @@ namespace Emby.Server.Implementations.IO
File.Copy(file1, temp1, true);
File.Copy(file2, file1, true);
- File.Copy(temp1, file2, true);
+ File.Move(temp1, file2, true);
}
///
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 0a3d740ccf..82db7c46b3 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -122,6 +122,7 @@ namespace Emby.Server.Implementations.Images
}
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
+ File.Delete(outputPath);
return ItemUpdateType.ImageUpdate;
}
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 04d90af3c3..f9c10ba098 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
@@ -33,12 +32,12 @@ namespace Emby.Server.Implementations.Images
Parent = item,
Recursive = true,
DtoOptions = new DtoOptions(true),
- ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new (ItemSortBy, SortOrder)[]
- {
+ ImageTypes = [ImageType.Primary],
+ OrderBy =
+ [
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
- },
+ ],
Limit = 1
});
}
diff --git a/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
index ce83673638..98e26a3222 100644
--- a/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
+++ b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
@@ -1,7 +1,10 @@
#pragma warning disable CS1591
+using System.Collections.Generic;
+using System.Linq;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -15,5 +18,13 @@ namespace Emby.Server.Implementations.Images
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
+
+ protected override IReadOnlyList 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();
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 665d70a418..b01fd93a7b 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library
}
///
- public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
{
// Don't ignore application folders
if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index cf6fc18456..a2301c8aed 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
}
};
- private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
+ private static readonly Glob[] _globs = Array.ConvertAll(_patterns, p => Glob.Parse(p, _globOptions));
///
/// Returns true if the supplied path should be ignored.
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a2abafd2ae..cbded1ec6a 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
#pragma warning disable CS1591
+#pragma warning disable CA5394
using System;
using System.Collections.Concurrent;
@@ -18,6 +17,7 @@ using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
+using Emby.Server.Implementations.Sorting;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -89,8 +89,8 @@ namespace Emby.Server.Implementations.Library
///
/// The _root folder.
///
- private volatile AggregateFolder _rootFolder;
- private volatile UserRootFolder _userRootFolder;
+ private volatile AggregateFolder? _rootFolder;
+ private volatile UserRootFolder? _userRootFolder;
private bool _wizardCompleted;
@@ -155,17 +155,17 @@ namespace Emby.Server.Implementations.Library
///
/// Occurs when [item added].
///
- public event EventHandler ItemAdded;
+ public event EventHandler? ItemAdded;
///
/// Occurs when [item updated].
///
- public event EventHandler ItemUpdated;
+ public event EventHandler? ItemUpdated;
///
/// Occurs when [item removed].
///
- public event EventHandler ItemRemoved;
+ public event EventHandler? ItemRemoved;
///
/// Gets the root folder.
@@ -264,7 +264,7 @@ namespace Emby.Server.Implementations.Library
///
/// The sender.
/// The instance containing the event data.
- private void ConfigurationUpdated(object sender, EventArgs e)
+ private void ConfigurationUpdated(object? sender, EventArgs e)
{
var config = _configurationManager.Configuration;
@@ -338,7 +338,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
- "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -347,7 +347,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
- "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -366,7 +366,7 @@ namespace Emby.Server.Implementations.Library
}
_logger.LogDebug(
- "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
@@ -395,7 +395,7 @@ namespace Emby.Server.Implementations.Library
try
{
_logger.LogInformation(
- "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
@@ -410,6 +410,24 @@ namespace Emby.Server.Implementations.Library
File.Delete(fileSystemInfo.FullName);
}
}
+ catch (DirectoryNotFoundException)
+ {
+ _logger.LogInformation(
+ "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+ }
+ catch (FileNotFoundException)
+ {
+ _logger.LogInformation(
+ "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+ }
catch (IOException)
{
if (isRequiredForDelete)
@@ -443,7 +461,7 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
- private static IEnumerable GetMetadataPaths(BaseItem item, IEnumerable children)
+ private static List GetMetadataPaths(BaseItem item, IEnumerable children)
{
var list = new List
{
@@ -461,7 +479,7 @@ namespace Emby.Server.Implementations.Library
/// The args.
/// The resolvers.
/// BaseItem.
- private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers)
+ private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
{
var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
.FirstOrDefault(i => i is not null);
@@ -474,7 +492,7 @@ namespace Emby.Server.Implementations.Library
return item;
}
- private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver)
+ private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
{
try
{
@@ -516,16 +534,16 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5();
}
- public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
+ public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
- private BaseItem ResolvePath(
+ private BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
IDirectoryService directoryService,
- IItemResolver[] resolvers,
- Folder parent = null,
+ IItemResolver[]? resolvers,
+ Folder? parent = null,
CollectionType? collectionType = null,
- LibraryOptions libraryOptions = null)
+ LibraryOptions? libraryOptions = null)
{
ArgumentNullException.ThrowIfNull(fileInfo);
@@ -598,7 +616,7 @@ namespace Emby.Server.Implementations.Library
return ResolveItem(args, resolvers);
}
- public bool IgnoreFile(FileSystemMetadata file, BaseItem parent)
+ public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
=> EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
public List NormalizeRootPathList(IEnumerable paths)
@@ -673,16 +691,16 @@ namespace Emby.Server.Implementations.Library
private IEnumerable ResolveFileList(
IReadOnlyList fileList,
IDirectoryService directoryService,
- Folder parent,
+ Folder? parent,
CollectionType? collectionType,
- IItemResolver[] resolvers,
+ IItemResolver[]? resolvers,
LibraryOptions libraryOptions)
{
// Given that fileList is a list we can save enumerator allocations by indexing
for (var i = 0; i < fileList.Count; i++)
{
var file = fileList[i];
- BaseItem result = null;
+ BaseItem? result = null;
try
{
result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
@@ -711,7 +729,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(rootFolderPath);
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
- ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
+ (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong"))
.DeepCopy();
// In case program data folder was moved
@@ -777,7 +795,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(userRootPath);
var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
- UserRootFolder tmpItem = null;
+ UserRootFolder? tmpItem = null;
try
{
tmpItem = GetItemById(newItemId) as UserRootFolder;
@@ -790,7 +808,8 @@ namespace Emby.Server.Implementations.Library
if (tmpItem is null)
{
_logger.LogDebug("Creating new userRootFolder with DeepCopy");
- tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy();
+ tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new InvalidOperationException("Failed to get user root path"))
+ .DeepCopy();
}
// In case program data folder was moved
@@ -809,7 +828,8 @@ namespace Emby.Server.Implementations.Library
return _userRootFolder;
}
- public BaseItem FindByPath(string path, bool? isFolder)
+ ///
+ public BaseItem? FindByPath(string path, bool? isFolder)
{
// If this returns multiple items it could be tricky figuring out which one is correct.
// In most cases, the newest one will be and the others obsolete but not yet cleaned up
@@ -828,12 +848,8 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault();
}
- ///
- /// Gets the person.
- ///
- /// The name.
- /// Task{Person}.
- public Person GetPerson(string name)
+ ///
+ public Person? GetPerson(string name)
{
var path = Person.GetPath(name);
var id = GetItemByNameId(path);
@@ -1015,7 +1031,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken)
+ public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -1024,7 +1040,8 @@ namespace Emby.Server.Implementations.Library
new Progress(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
- cancellationToken).ConfigureAwait(false);
+ allowRemoveRoot: removeRoot,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -1032,7 +1049,8 @@ namespace Emby.Server.Implementations.Library
new Progress(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
- cancellationToken).ConfigureAwait(false);
+ allowRemoveRoot: removeRoot,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType())
@@ -1050,7 +1068,7 @@ namespace Emby.Server.Implementations.Library
var innerProgress = new Progress(pct => progress.Report(pct * 0.96));
// Validate the entire media library
- await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
+ await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken: cancellationToken).ConfigureAwait(false);
progress.Report(96);
@@ -1140,7 +1158,7 @@ namespace Emby.Server.Implementations.Library
.ToList();
}
- private VirtualFolderInfo GetVirtualFolderInfo(string dir, List allCollectionFolders, HashSet refreshQueue)
+ private VirtualFolderInfo GetVirtualFolderInfo(string dir, List allCollectionFolders, HashSet? refreshQueue)
{
var info = new VirtualFolderInfo
{
@@ -1204,20 +1222,15 @@ namespace Emby.Server.Implementations.Library
return null;
}
- ///
- /// Gets the item by id.
- ///
- /// The id.
- /// BaseItem.
- /// is null.
- public BaseItem GetItemById(Guid id)
+ ///
+ public BaseItem? GetItemById(Guid id)
{
if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_cache.TryGetValue(id, out BaseItem item))
+ if (_cache.TryGetValue(id, out BaseItem? item))
{
return item;
}
@@ -1233,7 +1246,7 @@ namespace Emby.Server.Implementations.Library
}
///
- public T GetItemById(Guid id)
+ public T? GetItemById(Guid id)
where T : BaseItem
{
var item = GetItemById(id);
@@ -1245,6 +1258,22 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ ///
+ public T? GetItemById(Guid id, Guid userId)
+ where T : BaseItem
+ {
+ var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
+ return GetItemById(id, user);
+ }
+
+ ///
+ public T? GetItemById(Guid id, User? user)
+ where T : BaseItem
+ {
+ var item = GetItemById(id);
+ return ItemIsVisible(item, user) ? item : null;
+ }
+
public List GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
@@ -1405,7 +1434,7 @@ namespace Emby.Server.Implementations.Library
var parents = new BaseItem[len];
for (int i = 0; i < len; i++)
{
- parents[i] = GetItemById(ancestorIds[i]);
+ parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id: {ancestorIds[i]}");
if (parents[i] is not (ICollectionFolder or UserView))
{
return;
@@ -1419,7 +1448,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid() };
+ query.TopParentIds = [Guid.NewGuid()];
}
}
@@ -1516,7 +1545,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user)
+ private IEnumerable GetTopParentIdsForQuery(BaseItem item, User? user)
{
if (item is UserView view)
{
@@ -1585,16 +1614,20 @@ namespace Emby.Server.Implementations.Library
/// IEnumerable{System.String}.
public async Task> GetIntros(BaseItem item, User user)
{
+ if (IntroProviders.Length == 0)
+ {
+ return [];
+ }
+
var tasks = IntroProviders
- .Take(1)
.Select(i => GetIntros(i, item, user));
var items = await Task.WhenAll(tasks).ConfigureAwait(false);
return items
- .SelectMany(i => i.ToArray())
+ .SelectMany(i => i)
.Select(ResolveIntro)
- .Where(i => i is not null);
+ .Where(i => i is not null)!; // null values got filtered out
}
///
@@ -1623,9 +1656,9 @@ namespace Emby.Server.Implementations.Library
///
/// The info.
/// Video.
- private Video ResolveIntro(IntroInfo info)
+ private Video? ResolveIntro(IntroInfo info)
{
- Video video = null;
+ Video? video = null;
if (info.ItemId.HasValue)
{
@@ -1676,42 +1709,42 @@ namespace Emby.Server.Implementations.Library
return video;
}
- ///
- /// Sorts the specified sort by.
- ///
- /// The items.
- /// The user.
- /// The sort by.
- /// The sort order.
- /// IEnumerable{BaseItem}.
- public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder)
+ ///
+ public IEnumerable Sort(IEnumerable items, User? user, IEnumerable sortBy, SortOrder sortOrder)
{
- var isFirst = true;
-
- IOrderedEnumerable orderedItems = null;
+ IOrderedEnumerable? orderedItems = null;
foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
{
- if (isFirst)
+ if (orderBy is RandomComparer)
{
- orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
+ var randomItems = items.ToArray();
+ Random.Shared.Shuffle(randomItems);
+ items = randomItems;
+ // Items are no longer ordered at this point, so set orderedItems back to null
+ orderedItems = null;
+ }
+ else if (orderedItems is null)
+ {
+ orderedItems = sortOrder == SortOrder.Descending
+ ? items.OrderByDescending(i => i, orderBy)
+ : items.OrderBy(i => i, orderBy);
}
else
{
- orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
+ orderedItems = sortOrder == SortOrder.Descending
+ ? orderedItems!.ThenByDescending(i => i, orderBy)
+ : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
}
-
- isFirst = false;
}
return orderedItems ?? items;
}
- public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
+ ///
+ public IEnumerable Sort(IEnumerable items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
- var isFirst = true;
-
- IOrderedEnumerable orderedItems = null;
+ IOrderedEnumerable? orderedItems = null;
foreach (var (name, sortOrder) in orderBy)
{
@@ -1721,16 +1754,26 @@ namespace Emby.Server.Implementations.Library
continue;
}
- if (isFirst)
+ if (comparer is RandomComparer)
{
- orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
+ var randomItems = items.ToArray();
+ Random.Shared.Shuffle(randomItems);
+ items = randomItems;
+ // Items are no longer ordered at this point, so set orderedItems back to null
+ orderedItems = null;
+ }
+ else if (orderedItems is null)
+ {
+ orderedItems = sortOrder == SortOrder.Descending
+ ? items.OrderByDescending(i => i, comparer)
+ : items.OrderBy(i => i, comparer);
}
else
{
- orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, comparer) : orderedItems.ThenBy(i => i, comparer);
+ orderedItems = sortOrder == SortOrder.Descending
+ ? orderedItems!.ThenByDescending(i => i, comparer)
+ : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
}
-
- isFirst = false;
}
return orderedItems ?? items;
@@ -1742,14 +1785,14 @@ namespace Emby.Server.Implementations.Library
/// The name.
/// The user.
/// IBaseItemComparer.
- private IBaseItemComparer GetComparer(ItemSortBy name, User user)
+ private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
{
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
{
- var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType());
+ var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable instances
userComparer.User = user;
userComparer.UserManager = _userManager;
@@ -1761,23 +1804,14 @@ namespace Emby.Server.Implementations.Library
return comparer;
}
- ///
- /// Creates the item.
- ///
- /// The item.
- /// The parent item.
- public void CreateItem(BaseItem item, BaseItem parent)
+ ///
+ public void CreateItem(BaseItem item, BaseItem? parent)
{
CreateItems(new[] { item }, parent, CancellationToken.None);
}
- ///
- /// Creates the items.
- ///
- /// The items.
- /// The parent item.
- /// The cancellation token.
- public void CreateItems(IReadOnlyList items, BaseItem parent, CancellationToken cancellationToken)
+ ///
+ public void CreateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
@@ -1860,7 +1894,7 @@ namespace Emby.Server.Implementations.Library
try
{
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)
{
@@ -2059,16 +2093,16 @@ namespace Emby.Server.Implementations.Library
public LibraryOptions GetLibraryOptions(BaseItem item)
{
- if (item is not CollectionFolder collectionFolder)
+ if (item is CollectionFolder collectionFolder)
{
- // List.Find is more performant than FirstOrDefault due to enumerator allocation
- collectionFolder = GetCollectionFolders(item)
- .Find(folder => folder is CollectionFolder) as CollectionFolder;
+ return collectionFolder.GetLibraryOptions();
}
- return collectionFolder is null
- ? new LibraryOptions()
- : collectionFolder.GetLibraryOptions();
+ // List.Find is more performant than FirstOrDefault due to enumerator allocation
+ return GetCollectionFolders(item)
+ .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
+ ? collectionFolder2.GetLibraryOptions()
+ : new LibraryOptions();
}
public CollectionType? GetContentType(BaseItem item)
@@ -2422,7 +2456,7 @@ namespace Emby.Server.Implementations.Library
{
if (parentId.HasValue)
{
- return GetItemById(parentId.Value);
+ return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}");
}
if (!userId.IsNullOrEmpty())
@@ -2459,7 +2493,7 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
- EpisodeInfo episodeInfo = null;
+ EpisodeInfo? episodeInfo = null;
if (episode.IsFileProtocol)
{
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
@@ -2662,7 +2696,7 @@ namespace Emby.Server.Implementations.Library
}
}
- BaseItem GetExtra(FileSystemMetadata file, ExtraType extraType)
+ BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType)
{
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
if (extra is not Video && extra is not Audio)
@@ -2677,16 +2711,21 @@ namespace Emby.Server.Implementations.Library
extra = itemById;
}
- extra.ExtraType = extraType;
+ // Only update extra type if it is more specific then the currently known extra type
+ if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
+ {
+ extra.ExtraType = extraType;
+ }
+
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
return extra;
}
}
- public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
+ public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
{
- string newPath;
+ string? newPath;
if (ownerItem is not null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2760,8 +2799,8 @@ namespace Emby.Server.Implementations.Library
}
})
.Where(i => i is not null)
- .Where(i => query.User is null || i.IsVisible(query.User))
- .ToList();
+ .Where(i => query.User is null || i!.IsVisible(query.User))
+ .ToList()!; // null values are filtered out
}
public List GetPeopleNames(InternalPeopleQuery query)
@@ -2783,8 +2822,10 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.UpdatePeople(item.Id, people);
-
- await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
+ if (people is not null)
+ {
+ await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
+ }
}
public async Task ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure)
@@ -2863,7 +2904,7 @@ namespace Emby.Server.Implementations.Library
if (collectionType is not null)
{
- var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
+ var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
await File.WriteAllBytesAsync(path, Array.Empty()).ConfigureAwait(false);
}
@@ -2897,7 +2938,7 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable people, CancellationToken cancellationToken)
{
- List personsToSave = null;
+ List? personsToSave = null;
foreach (var person in people)
{
@@ -3010,9 +3051,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
- var list = libraryOptions.PathInfos.ToList();
- list.Add(pathInfo);
- libraryOptions.PathInfos = list.ToArray();
+ libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
@@ -3031,8 +3070,7 @@ namespace Emby.Server.Implementations.Library
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
- var list = libraryOptions.PathInfos.ToList();
- foreach (var originalPathInfo in list)
+ foreach (var originalPathInfo in libraryOptions.PathInfos)
{
if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
{
@@ -3041,8 +3079,6 @@ namespace Emby.Server.Implementations.Library
}
}
- libraryOptions.PathInfos = list.ToArray();
-
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
}
@@ -3095,7 +3131,7 @@ namespace Emby.Server.Implementations.Library
if (refreshLibrary)
{
- await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
+ await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
StartScanInBackground();
}
@@ -3115,7 +3151,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(path));
}
- List removeList = null;
+ List? removeList = null;
foreach (var contentType in _configurationManager.Configuration.ContentTypes)
{
@@ -3168,5 +3204,20 @@ namespace Emby.Server.Implementations.Library
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
}
+
+ private static bool ItemIsVisible(BaseItem? item, User? user)
+ {
+ if (item is null)
+ {
+ return false;
+ }
+
+ if (user is null)
+ {
+ return true;
+ }
+
+ return item is UserRootFolder || item.IsVisibleStandalone(user);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 18ada6aeb5..bb22ca82fa 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -113,6 +113,11 @@ namespace Emby.Server.Implementations.Library
return true;
}
+ if (stream.IsPgsSubtitleStream)
+ {
+ return true;
+ }
+
return false;
}
@@ -191,7 +196,7 @@ namespace Emby.Server.Implementations.Library
if (user is not null)
{
- SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
+ SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
if (item.MediaType == MediaType.Audio)
{
@@ -274,7 +279,7 @@ namespace Emby.Server.Implementations.Library
var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
- return results.SelectMany(i => i.ToList());
+ return results.SelectMany(i => i);
}
private async Task> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken)
@@ -296,7 +301,7 @@ namespace Emby.Server.Implementations.Library
catch (Exception ex)
{
_logger.LogError(ex, "Error getting media sources");
- return Enumerable.Empty();
+ return [];
}
}
@@ -339,7 +344,7 @@ namespace Emby.Server.Implementations.Library
{
foreach (var source in sources)
{
- SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
+ SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
if (item.MediaType == MediaType.Audio)
{
@@ -360,7 +365,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.IsNullOrEmpty(language))
{
- return Array.Empty();
+ return [];
}
var culture = _localizationManager.FindLanguageInfo(language);
@@ -369,14 +374,15 @@ namespace Emby.Server.Implementations.Library
return culture.ThreeLetterISOLanguageNames;
}
- return new string[] { language };
+ return [language];
}
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData.SubtitleStreamIndex.HasValue
&& user.RememberSubtitleSelections
- && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+ && user.SubtitleMode != SubtitlePlaybackMode.None
+ && allowRememberingSelection)
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
@@ -390,7 +396,7 @@ namespace Emby.Server.Implementations.Library
var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
- var audioLangage = defaultAudioIndex is null
+ var audioLanguage = defaultAudioIndex is null
? null
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
@@ -398,9 +404,9 @@ namespace Emby.Server.Implementations.Library
source.MediaStreams,
preferredSubs,
user.SubtitleMode,
- audioLangage);
+ audioLanguage);
- MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
+ MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
@@ -421,7 +427,7 @@ namespace Emby.Server.Implementations.Library
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
- public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
+ public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
var mediaType = item?.MediaType ?? MediaType.Video;
@@ -526,7 +532,7 @@ namespace Emby.Server.Implementations.Library
var item = request.ItemId.IsEmpty()
? null
: _libraryManager.GetItemById(request.ItemId);
- SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
+ SetDefaultAudioAndSubtitleStreamIndices(item, clone, user);
}
return new Tuple(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 6aef87c525..ea223e3ece 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -124,16 +124,16 @@ namespace Emby.Server.Implementations.Library
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // always load the most suitable full subtitles
+ // Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // always load the most suitable full subtitles
+ // Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
}
- // load forced subs if we have found no suitable full subtitles
+ // Load forced subs if we have found no suitable full subtitles
var iterStreams = filteredStreams is null || filteredStreams.Count == 0
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index c4b6b37561..21e7079d88 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Library
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
- // Must be at least 3 characters after the attribute =, ], any character.
- var maxIndex = str.Length - attribute.Length - 3;
+ // Must be at least 3 characters after the attribute =, ], any character,
+ // then we offset it by 1, because we want the index and not length.
+ var maxIndex = str.Length - attribute.Length - 2;
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 7a61e2607c..c9e3a4daf8 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -35,11 +35,11 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
- item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values
- var fileInfo = directoryService.GetFile(item.Path);
+ var fileInfo = directoryService.GetFileSystemEntry(item.Path);
if (fileInfo is null)
{
return false;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 0bfb7fbe6a..9405f21027 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 1bdae7f62b..f7270bec14 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
+using Emby.Naming.Audio;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
@@ -85,6 +86,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
+ var albumParser = new AlbumParser(_namingOptions);
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 (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 6cc04ea810..955055313e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml"))
+ if (filename.Contains("[boxset]", StringComparison.OrdinalIgnoreCase) || args.ContainsFileSystemEntryByName("collection.xml"))
{
return new BoxSet
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a50435ae69..a03c1214d6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -11,7 +9,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.LocalMetadata.Savers;
-using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
{
@@ -20,11 +17,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
///
public class PlaylistResolver : GenericFolderResolver
{
- private CollectionType?[] _musicPlaylistCollectionTypes =
- {
+ private readonly CollectionType?[] _musicPlaylistCollectionTypes =
+ [
null,
CollectionType.music
- };
+ ];
///
protected override Playlist Resolve(ItemResolveArgs args)
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 858c5b2812..abf2d01159 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id,
- SeriesName = series.Name
+ SeriesName = series.Name,
+ Path = seasonParserResult.IsSeasonFolder ? path : null
};
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
@@ -78,27 +79,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
}
}
- if (season.IndexNumber.HasValue)
+ if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
{
var seasonNumber = season.IndexNumber.Value;
- if (string.IsNullOrEmpty(season.Name))
- {
- var seasonNames = series.SeasonNames;
- if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
- {
- season.Name = seasonName;
- }
- else
- {
- season.Name = seasonNumber == 0 ?
- args.LibraryOptions.SeasonZeroDisplayName :
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameSeasonNumber"),
- seasonNumber,
- args.LibraryOptions.PreferredMetadataLanguage);
- }
- }
+ season.Name = seasonNumber == 0 ?
+ args.LibraryOptions.SeasonZeroDisplayName :
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("NameSeasonNumber"),
+ seasonNumber,
+ args.LibraryOptions.PreferredMetadataLanguage);
}
return season;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 83a66c8e47..d9a5590140 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -303,8 +303,8 @@ namespace Emby.Server.Implementations.Library
{
// Handle situations with the grouping setting, e.g. movies showing up in tv, etc.
// Thanks to mixed content libraries included in the UserView
- var hasCollectionType = parents.OfType().ToArray();
- if (hasCollectionType.Length > 0)
+ var hasCollectionType = parents.OfType().ToList();
+ if (hasCollectionType.Count > 0)
{
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 601aab5b94..725b8f76c7 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -64,6 +64,11 @@ namespace Emby.Server.Implementations.Library.Validators
try
{
var item = _libraryManager.GetPerson(person);
+ if (item is null)
+ {
+ _logger.LogWarning("Failed to get person: {Name}", person);
+ continue;
+ }
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -92,7 +97,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { BaseItemKind.Person },
+ IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json
index 0967ef424b..bc6062f429 100644
--- a/Emby.Server.Implementations/Localization/Core/ab.json
+++ b/Emby.Server.Implementations/Localization/Core/ab.json
@@ -1 +1,3 @@
-{}
+{
+ "Albums": "аальбомқәа"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index ecea8df6a8..e89ede10b4 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -5,12 +5,12 @@
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
- "HeaderAlbumArtists": "Kunstenaars se Album",
+ "HeaderAlbumArtists": "Album kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Flieks",
"Shows": "Televisie Reekse",
- "HeaderContinueWatching": "Kyk Verder",
+ "HeaderContinueWatching": "Hou aan kyk",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Foto's",
"Playlists": "Snitlyste",
@@ -19,7 +19,7 @@
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
- "DeviceOnlineWithName": "{0} is gekoppel",
+ "DeviceOnlineWithName": "{0} is aanlyn",
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
@@ -61,7 +61,7 @@
"NotificationOptionPluginInstalled": "Inprop module geïnstalleer",
"NotificationOptionPluginError": "Inprop module het misluk",
"NotificationOptionNewLibraryContent": "Nuwe inhoud bygevoeg",
- "NotificationOptionInstallationFailed": "Installering het misluk",
+ "NotificationOptionInstallationFailed": "Installasie mislukking",
"NotificationOptionCameraImageUploaded": "Kamera foto is opgelaai",
"NotificationOptionAudioPlaybackStopped": "Oudio terugspeel het gestop",
"NotificationOptionAudioPlayback": "Oudio terugspeel het begin",
@@ -86,9 +86,9 @@
"HomeVideos": "Tuis Videos",
"HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres",
- "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
+ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"ChapterNameValue": "Hoofstuk {0}",
- "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
+ "CameraImageUploadedFrom": "'n Nuwe kamera foto is opgelaai vanaf {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums",
"TasksChannelsCategory": "Internet kanale",
@@ -114,8 +114,8 @@
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
"TaskRefreshChapterImages": "Verkry Hoofstuk Beelde",
"Undefined": "Ongedefineerd",
- "Forced": "Geforseer",
- "Default": "Oorspronklik",
+ "Forced": "Geforseerd",
+ "Default": "Standaard",
"TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
"TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon",
"TaskOptimizeDatabaseDescription": "Komprimeer databasis en verkort vrye ruimte. As hierdie taak uitgevoer word nadat die media versameling geskandeer is of ander veranderings aangebring is wat databasisaanpassings impliseer, kan dit die prestasie verbeter.",
@@ -125,5 +125,9 @@
"External": "Ekstern",
"HearingImpaired": "gehoorgestremd",
"TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde",
- "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling."
+ "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
+ "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
+ "TaskAudioNormalization": "Odio Normalisering",
+ "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 35387d0325..4245656ff9 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -11,7 +11,7 @@
"Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
- "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
+ "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "التصنيفات",
@@ -126,5 +126,9 @@
"External": "خارجي",
"HearingImpaired": "ضعاف السمع",
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
- "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة."
+ "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
+ "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
+ "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
+ "TaskAudioNormalization": "تطبيع الصوت",
+ "TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت."
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 05af8d8a5a..9172af516e 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -52,7 +52,7 @@
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
"TaskOptimizeDatabase": "Аптымізаваць базу дадзеных",
"Artists": "Выканаўцы",
- "UserOfflineFromDevice": "{0} адключыўся ад {1}",
+ "UserOfflineFromDevice": "{0} адлучыўся ад {1}",
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
"TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.",
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
@@ -66,7 +66,7 @@
"AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}",
"Books": "Кнігі",
"CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
- "DeviceOfflineWithName": "{0} адключыўся",
+ "DeviceOfflineWithName": "{0} адлучыўся",
"DeviceOnlineWithName": "{0} падлучаны",
"Forced": "Прымусова",
"HeaderRecordingGroups": "Групы запісаў",
@@ -125,5 +125,9 @@
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
+ "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
+ "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
+ "TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
+ "TaskAudioNormalization": "Нармалізацыя гуку"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index c4d8c69479..2998489b57 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -126,5 +126,9 @@
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
- "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades."
+ "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
+ "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
+ "TaskAudioNormalization": "Normalització d'Àudio",
+ "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 1c7bc75b5c..14cfeb71af 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -22,7 +22,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba",
- "HeaderLiveTV": "Živý přenos",
+ "HeaderLiveTV": "TV vysílání",
"HeaderNextUp": "Další díly",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa",
@@ -126,5 +126,9 @@
"External": "Externí",
"HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
+ "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
+ "TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
+ "TaskAudioNormalization": "Normalizace zvuku",
+ "TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku."
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 092af34b6b..e871a4362d 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -17,7 +17,7 @@
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favoritalbummer",
+ "HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Yndlingsserier",
@@ -87,21 +87,21 @@
"UserOnlineFromDevice": "{0} er online fra {1}",
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
- "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
+ "UserStartedPlayingItemWithValues": "{0} afspiller {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
- "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
+ "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Mediebibliotek",
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
- "TaskCleanCache": "Ryd Cache-mappe",
+ "TaskCleanCache": "Ryd cache-mappe",
"TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
@@ -126,5 +126,9 @@
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet",
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
- "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker."
+ "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
+ "TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
+ "TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
+ "TaskAudioNormalization": "Audio-normalisering"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 7a4c2067ba..ce98979e6e 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -126,5 +126,9 @@
"External": "Extern",
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
- "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
+ "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
+ "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
+ "TaskAudioNormalization": "Audio Normalisierung",
+ "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten."
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 5ea6a22527..056a2e4755 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -126,5 +126,9 @@
"External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής",
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
+ "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
+ "TaskAudioNormalization": "Ομοιομορφία ήχου",
+ "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
+ "TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 32bf893100..75285fe8e1 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -126,5 +126,9 @@
"External": "External",
"HearingImpaired": "Hearing Impaired",
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
- "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
+ "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
+ "TaskAudioNormalization": "Audio Normalisation",
+ "TaskAudioNormalizationDescription": "Scans files for audio normalisation data."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 4ba31bee03..1a69627fa3 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -13,7 +13,7 @@
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"External": "External",
- "FailedLoginAttemptWithUserName": "Failed login try from {0}",
+ "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Forced": "Forced",
@@ -106,6 +106,8 @@
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images",
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
+ "TaskAudioNormalization": "Audio Normalization",
+ "TaskAudioNormalizationDescription": "Scans files for audio normalization data.",
"TaskRefreshLibrary": "Scan Media Library",
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
"TaskCleanLogs": "Clean Log Directory",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index d677cc46c7..e9ace71a5e 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -11,7 +11,7 @@
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
- "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
+ "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
@@ -124,5 +124,11 @@
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
"External": "Externo",
- "HearingImpaired": "Discapacidad Auditiva"
+ "HearingImpaired": "Discapacidad Auditiva",
+ "TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
+ "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
+ "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index fe10be3085..13e007b4c2 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -11,7 +11,7 @@
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
- "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
+ "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
@@ -30,7 +30,7 @@
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
- "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
+ "LabelRunningTimeValue": "Duración: {0}",
"Latest": "Últimas",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
@@ -126,5 +126,9 @@
"External": "Externo",
"HearingImpaired": "Discapacidad Auditiva",
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
- "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas."
+ "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
+ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index c6863ff368..e7deefbb09 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -112,7 +112,7 @@
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación",
- "AppDeviceValues": "App: {0}, Dispositivo: {1}",
+ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Discapacidad auditiva",
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
- "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción"
+ "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
+ "TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
+ "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 0f4c7438fa..8cdd06b7c4 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -12,14 +12,118 @@
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
- "HeaderAlbumArtists": "Artistas del Álbum",
+ "HeaderAlbumArtists": "Artistas del álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
- "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
+ "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
"HeaderFavoriteSongs": "Canciones Favoritas",
"HeaderFavoriteEpisodes": "Episodios Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"External": "Externo",
- "Default": "Predeterminado"
+ "Default": "Predeterminado",
+ "Movies": "Películas",
+ "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
+ "MixedContent": "Contenido mixto",
+ "Music": "Música",
+ "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
+ "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
+ "NotificationOptionVideoPlayback": "Reproducción de video iniciada",
+ "Sync": "Sincronizar",
+ "Shows": "Series",
+ "UserDownloadingItemWithValues": "{0} está descargando {1}",
+ "UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
+ "UserOnlineFromDevice": "{0} está en línea desde {1}",
+ "TasksChannelsCategory": "Canales de Internet",
+ "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
+ "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
+ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
+ "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
+ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
+ "TvShows": "Series de TV",
+ "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
+ "TaskRefreshChannels": "Actualizar canales",
+ "Photos": "Fotos",
+ "HeaderFavoriteShows": "Programas favoritos",
+ "TaskCleanActivityLog": "Limpiar registro de actividades",
+ "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
+ "System": "Sistema",
+ "User": "Usuario",
+ "Forced": "Forzado",
+ "PluginInstalledWithName": "{0} ha sido instalado",
+ "HeaderFavoriteAlbums": "Álbumes favoritos",
+ "TaskUpdatePlugins": "Actualizar Plugins",
+ "Latest": "Recientes",
+ "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
+ "Songs": "Canciones",
+ "NotificationOptionPluginError": "Falla de plugin",
+ "ScheduledTaskStartedWithName": "{0} iniciado",
+ "TasksApplicationCategory": "Aplicación",
+ "UserDeletedWithName": "El usuario {0} ha sido eliminado",
+ "TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
+ "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para plugins que están configurados para actualizarse automáticamente.",
+ "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
+ "NotificationOptionUserLockedOut": "Usuario bloqueado",
+ "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+ "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
+ "NotificationOptionPluginUpdateInstalled": "Actualización de plugin instalada",
+ "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
+ "TasksLibraryCategory": "Biblioteca",
+ "NotificationOptionPluginInstalled": "Plugin instalado",
+ "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
+ "VersionNumber": "Versión {0}",
+ "HeaderNextUp": "A continuación",
+ "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
+ "LabelIpAddressValue": "Dirección IP: {0}",
+ "NameSeasonNumber": "Temporada {0}",
+ "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
+ "Plugin": "Plugin",
+ "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
+ "NotificationOptionTaskFailed": "Falló la tarea programada",
+ "LabelRunningTimeValue": "Tiempo en ejecución: {0}",
+ "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
+ "TaskRefreshLibrary": "Escanear biblioteca de medios",
+ "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
+ "TasksMaintenanceCategory": "Mantenimiento",
+ "ProviderValue": "Proveedor: {0}",
+ "UserCreatedWithName": "El usuario {0} ha sido creado",
+ "PluginUninstalledWithName": "{0} ha sido desinstalado",
+ "ValueSpecialEpisodeName": "Especial - {0}",
+ "ScheduledTaskFailedWithName": "{0} falló",
+ "TaskCleanLogs": "Limpiar directorio de registros",
+ "NameInstallFailed": "Falló la instalación de {0}",
+ "UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
+ "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
+ "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
+ "Playlists": "Listas de reproducción",
+ "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
+ "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
+ "TaskRefreshPeople": "Actualizar personas",
+ "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
+ "HeaderLiveTV": "TV en vivo",
+ "NameSeasonUnknown": "Temporada desconocida",
+ "NotificationOptionInstallationFailed": "Fallo de instalación",
+ "NotificationOptionPluginUninstalled": "Plugin desinstalado",
+ "TaskCleanCache": "Limpiar directorio caché",
+ "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
+ "Inherit": "Heredar",
+ "HeaderRecordingGroups": "Grupos de grabación",
+ "ItemAddedWithName": "{0} fue agregado a la biblioteca",
+ "TaskOptimizeDatabase": "Optimizar base de datos",
+ "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
+ "HearingImpaired": "Discapacidad auditiva",
+ "HomeVideos": "Videos caseros",
+ "ItemRemovedWithName": "{0} fue removido de la biblioteca",
+ "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
+ "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
+ "MusicVideos": "Videos musicales",
+ "NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
+ "PluginUpdatedWithName": "{0} ha sido actualizado",
+ "Undefined": "Sin definir",
+ "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
+ "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
+ "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad."
}
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index 977307b065..075bcc9a44 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -125,5 +125,9 @@
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
- "TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
+ "TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
+ "TaskAudioNormalization": "Heli Normaliseerimine",
+ "TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.",
+ "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 8364ce2360..ce5177d1f9 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -126,5 +126,7 @@
"External": "خارجی",
"HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
- "TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه."
+ "TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
+ "TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
+ "TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index cba036ff47..dced61c5ed 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -125,5 +125,9 @@
"External": "Ulkoinen",
"HearingImpaired": "Kuulorajoitteinen",
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
- "TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista."
+ "TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
+ "TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
+ "TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
+ "TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index b816738c2c..42027dfb22 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -11,7 +11,7 @@
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
- "FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
+ "FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
@@ -39,7 +39,7 @@
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
- "MusicVideos": "Vidéos musicales",
+ "MusicVideos": "Vidéoclips",
"NameInstallFailed": "échec d'installation de {0}",
"NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue",
@@ -126,5 +126,9 @@
"External": "Externe",
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
+ "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
+ "TaskAudioNormalization": "Normalisation audio",
+ "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index d04a79de18..a13ee48d5b 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -126,5 +126,9 @@
"External": "Externe",
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
+ "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
+ "TaskAudioNormalization": "Normalisation audio",
+ "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json
index 28e54bff57..b511ed6ba9 100644
--- a/Emby.Server.Implementations/Localization/Core/ga.json
+++ b/Emby.Server.Implementations/Localization/Core/ga.json
@@ -1,3 +1,16 @@
{
- "Albums": "Albaim"
+ "Albums": "Albaim",
+ "Artists": "Ealaíontóir",
+ "AuthenticationSucceededWithUserName": "{0} fíordheimhnithe",
+ "Books": "leabhair",
+ "CameraImageUploadedFrom": "Tá íomhá ceamara nua uaslódáilte ó {0}",
+ "Channels": "Cainéil",
+ "ChapterNameValue": "Caibidil {0}",
+ "Collections": "Bailiúcháin",
+ "Default": "Mainneachtain",
+ "DeviceOfflineWithName": "scoireadh {0}",
+ "DeviceOnlineWithName": "{0} ceangailte",
+ "External": "Forimeallach",
+ "FailedLoginAttemptWithUserName": "Iarracht ar theip ar fhíordheimhniú ó {0}",
+ "Favorites": "Ceanáin"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 26eab392e7..c8e0364248 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -126,5 +126,9 @@
"External": "חיצוני",
"HearingImpaired": "לקוי שמיעה",
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
- "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
+ "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
+ "TaskAudioNormalization": "נרמול שמע",
+ "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
+ "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
+ "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index a283522197..380c08e0d2 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -14,7 +14,7 @@
"Forced": "बलपूर्वक",
"Folders": "फ़ोल्डर",
"Favorites": "पसंदीदा",
- "FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ",
+ "FailedLoginAttemptWithUserName": "{0} से संप्रवेश असफल हुआ",
"DeviceOnlineWithName": "{0} कनेक्ट हो गया है",
"DeviceOfflineWithName": "{0} डिस्कनेक्ट हो गया है",
"Default": "प्राथमिक",
@@ -125,5 +125,7 @@
"TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।",
"TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।",
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
- "TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे."
+ "TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
+ "TaskAudioNormalization": "श्रव्य सामान्यीकरण",
+ "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 5bb2b7d4db..6a5b8c5615 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -126,5 +126,6 @@
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
"HearingImpaired": "Oštećen sluh",
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
- "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama."
+ "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
+ "TaskAudioNormalization": "Normalizacija zvuka"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index ba3d5872ad..31d6aaedb9 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -1,13 +1,13 @@
{
"Albums": "Albumok",
- "AppDeviceValues": "Program: {0}, eszköz: {1}",
+ "AppDeviceValues": "Program: {0}, Eszköz: {1}",
"Application": "Alkalmazás",
"Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
"Books": "Könyvek",
- "CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
+ "CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}",
"Channels": "Csatornák",
- "ChapterNameValue": "{0}. jelenet",
+ "ChapterNameValue": "Jelenet {0}",
"Collections": "Gyűjtemények",
"DeviceOfflineWithName": "{0} kijelentkezett",
"DeviceOnlineWithName": "{0} belépett",
@@ -15,27 +15,27 @@
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Genres": "Műfajok",
- "HeaderAlbumArtists": "Albumelőadók",
+ "HeaderAlbumArtists": "Album előadók",
"HeaderContinueWatching": "Megtekintés folytatása",
- "HeaderFavoriteAlbums": "Kedvenc albumok",
- "HeaderFavoriteArtists": "Kedvenc előadók",
- "HeaderFavoriteEpisodes": "Kedvenc epizódok",
- "HeaderFavoriteShows": "Kedvenc sorozatok",
- "HeaderFavoriteSongs": "Kedvenc számok",
+ "HeaderFavoriteAlbums": "Kedvenc Albumok",
+ "HeaderFavoriteArtists": "Kedvenc Előadók",
+ "HeaderFavoriteEpisodes": "Kedvenc Epizódok",
+ "HeaderFavoriteShows": "Kedvenc Sorozatok",
+ "HeaderFavoriteSongs": "Kedvenc Dalok",
"HeaderLiveTV": "Élő TV",
"HeaderNextUp": "Következik",
- "HeaderRecordingGroups": "Felvételi csoportok",
- "HomeVideos": "Házi videók",
+ "HeaderRecordingGroups": "Felvevő Csoportok",
+ "HomeVideos": "Otthoni Videók",
"Inherit": "Örökölt",
"ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
"ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
"LabelIpAddressValue": "IP-cím: {0}",
"LabelRunningTimeValue": "Lejátszási idő: {0}",
"Latest": "Legújabb",
- "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
+ "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett",
"MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
- "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
+ "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}",
+ "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett",
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
"Music": "Zenék",
@@ -46,7 +46,7 @@
"NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
"NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
"NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
- "NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
+ "NotificationOptionAudioPlayback": "Hanglejátszás elkezdődött",
"NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
"NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
"NotificationOptionInstallationFailed": "Telepítési hiba",
@@ -126,5 +126,9 @@
"External": "Külső",
"HearingImpaired": "Hallássérült",
"TaskRefreshTrickplayImages": "Trickplay képek generálása",
- "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
+ "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
+ "TaskAudioNormalization": "Hangerő Normalizáció",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
+ "TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.",
+ "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 78a4433480..b925a482b3 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -81,7 +81,7 @@
"Movies": "Film",
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
- "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
+ "FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung",
@@ -125,5 +125,9 @@
"External": "Luar",
"HearingImpaired": "Gangguan Pendengaran",
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
+ "TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
+ "TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
+ "TaskAudioNormalization": "Normalisasi Audio",
+ "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 0f1f0b3d24..6cb55760a0 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -17,7 +17,7 @@
"Genres": "Stefnur",
"Folders": "Möppur",
"Favorites": "Uppáhalds",
- "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
+ "FailedLoginAttemptWithUserName": "{0} mistókst að auðkenna sig",
"DeviceOnlineWithName": "{0} hefur tengst",
"DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn",
@@ -123,5 +123,11 @@
"TaskRefreshChapterImages": "Plokka kafla-myndir",
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
"Forced": "Þvingað",
- "External": "Útvær"
+ "External": "Útvær",
+ "TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir fyrir myndbönd í virkum söfnum.",
+ "TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
+ "TaskAudioNormalization": "Hljóðstöðlun",
+ "TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
+ "TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til."
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index a34bcc4907..0e694af02d 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -51,10 +51,10 @@
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
"NotificationOptionInstallationFailed": "Installazione fallita",
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
- "NotificationOptionPluginError": "Errore del Plug-in",
- "NotificationOptionPluginInstalled": "Plug-in installato",
- "NotificationOptionPluginUninstalled": "Plug-in disinstallato",
- "NotificationOptionPluginUpdateInstalled": "Aggiornamento del plug-in installato",
+ "NotificationOptionPluginError": "Errore del plugin",
+ "NotificationOptionPluginInstalled": "Plugin installato",
+ "NotificationOptionPluginUninstalled": "Plugin disinstallato",
+ "NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
"NotificationOptionUserLockedOut": "Utente bloccato",
@@ -68,10 +68,10 @@
"PluginUpdatedWithName": "{0} è stato aggiornato",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} fallito",
- "ScheduledTaskStartedWithName": "{0} avviati",
+ "ScheduledTaskStartedWithName": "{0} avviato",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV",
- "Songs": "Canzoni",
+ "Songs": "Brani",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
@@ -83,48 +83,52 @@
"UserDeletedWithName": "L'utente {0} è stato rimosso",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
- "UserOfflineFromDevice": "{0} si è disconnesso su {1}",
+ "UserOfflineFromDevice": "{0} si è disconnesso da {1}",
"UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
- "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di \"{1}\" su {2}",
+ "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
"ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
"ValueSpecialEpisodeName": "Speciale - {0}",
"VersionNumber": "Versione {0}",
- "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali Internet.",
+ "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali internet.",
"TaskDownloadMissingSubtitlesDescription": "Cerca su internet i sottotitoli mancanti basandosi sulle configurazioni dei metadati.",
"TaskDownloadMissingSubtitles": "Scarica i sottotitoli mancanti",
- "TaskRefreshChannels": "Aggiorna i canali",
- "TaskCleanTranscodeDescription": "Cancella i file di transcode più vecchi di un giorno.",
- "TaskCleanTranscode": "Svuota la cartella del transcoding",
- "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
- "TaskUpdatePlugins": "Aggiorna i Plugin",
- "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
- "TaskRefreshPeople": "Aggiornamento Persone",
+ "TaskRefreshChannels": "Aggiorna canali",
+ "TaskCleanTranscodeDescription": "Cancella i file di transcodifica più vecchi di un giorno.",
+ "TaskCleanTranscode": "Svuota la cartella della transcodifica",
+ "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin configurati per l'aggiornamento automatico.",
+ "TaskUpdatePlugins": "Aggiorna i plugin",
+ "TaskRefreshPeopleDescription": "Aggiorna i metadati degli attori e registi nella tua libreria.",
+ "TaskRefreshPeople": "Aggiorna Persone",
"TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
"TaskCleanLogs": "Pulisci la cartella dei log",
- "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
- "TaskRefreshLibrary": "Scan Librerie",
- "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
+ "TaskRefreshLibraryDescription": "Scansiona la libreria alla ricerca di nuovi file e aggiorna i metadati.",
+ "TaskRefreshLibrary": "Scansione della libreria",
+ "TaskRefreshChapterImagesDescription": "Crea le miniature per i video che hanno capitoli.",
"TaskRefreshChapterImages": "Estrai immagini capitolo",
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
- "TaskCleanCache": "Pulisci la directory della cache",
+ "TaskCleanCache": "Pulisci la cartella della cache",
"TasksChannelsCategory": "Canali su Internet",
"TasksApplicationCategory": "Applicazione",
"TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
- "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata.",
+ "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.",
"Undefined": "Non Definito",
"Forced": "Forzato",
"Default": "Predefinito",
- "TaskOptimizeDatabaseDescription": "Compatta Database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altri cambiamenti inerenti il database potrebbe aumentarne la performance.",
- "TaskOptimizeDatabase": "Ottimizza Database",
+ "TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
+ "TaskOptimizeDatabase": "Ottimizza database",
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno",
- "HearingImpaired": "con problemi di udito",
+ "HearingImpaired": "Non Udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate."
+ "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
+ "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
+ "TaskAudioNormalization": "Normalizzazione dell'audio",
+ "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index ab6988006a..c8ed7d0fbc 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -125,5 +125,9 @@
"External": "外部",
"HearingImpaired": "聴覚障害の方",
"TaskRefreshTrickplayImages": "トリックプレー画像を生成",
- "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。"
+ "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。",
+ "TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ",
+ "TaskAudioNormalization": "音声の正規化",
+ "TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
+ "TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 67dcf5b049..b91889594b 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -124,5 +124,6 @@
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
"TaskKeyframeExtractor": "키프레임 추출",
"External": "외부",
- "HearingImpaired": "청각 장애"
+ "HearingImpaired": "청각 장애",
+ "TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index e7279994bb..004ce68f58 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -126,5 +126,7 @@
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
- "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose."
+ "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
+ "TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 6e58ef8345..78c3d0a409 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -17,7 +17,7 @@
"Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
- "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
+ "ValueHasBeenAddedToLibrary": "{0} tika pievienots jūsu multvides bibliotēkai",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
"UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
@@ -76,7 +76,7 @@
"Genres": "Žanri",
"Folders": "Mapes",
"Favorites": "Izlase",
- "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+ "FailedLoginAttemptWithUserName": "Neveiksmīgs ielogošanos mēģinājums no {0}",
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
@@ -95,7 +95,7 @@
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
- "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
+ "TaskDownloadMissingSubtitlesDescription": "Meklē trūkstošus subtitrus internēta balstoties uz metadatu uzstādījumiem.",
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
"TaskRefreshChannels": "Atjaunot kanālus",
@@ -105,8 +105,8 @@
"TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
"TaskRefreshPeople": "Atjaunot cilvēkus",
- "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
- "TaskCleanLogs": "Iztīrīt logdatņu mapi",
+ "TaskCleanLogsDescription": "Nodzēš žurnāla ierakstus, kas ir senāki par {0} dienām.",
+ "TaskCleanLogs": "Iztīrīt žurnālu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
"TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
- "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās."
+ "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
+ "TaskAudioNormalization": "Audio normalizācija",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Noņem elemēntus no kolekcijām un atskaņošanas sarakstiem, kuri vairs neeksistē.",
+ "TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
+ "TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 0b50fa5298..5c34493816 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -6,7 +6,7 @@
"ChapterNameValue": "അധ്യായം {0}",
"DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
"DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
- "FailedLoginAttemptWithUserName": "{0} - എന്നതിൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
+ "FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
"Forced": "നിർബന്ധിച്ചു",
"HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
"HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
@@ -123,5 +123,11 @@
"HearingImpaired": "കേൾവി തകരാറുകൾ",
"External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
- "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
+ "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ",
+ "TaskCleanCollectionsAndPlaylistsDescription": "നിലവിലില്ലാത്ത ശേഖരങ്ങളിൽ നിന്നും പ്ലേലിസ്റ്റുകളിൽ നിന്നും ഇനങ്ങൾ നീക്കംചെയ്യുന്നു.",
+ "TaskCleanCollectionsAndPlaylists": "ശേഖരങ്ങളും പ്ലേലിസ്റ്റുകളും വൃത്തിയാക്കുക",
+ "TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക",
+ "TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.",
+ "TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക",
+ "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mt.json b/Emby.Server.Implementations/Localization/Core/mt.json
new file mode 100644
index 0000000000..c9e11165de
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/mt.json
@@ -0,0 +1,133 @@
+{
+ "Albums": "Albums",
+ "AppDeviceValues": "App: {0}, Apparat: {1}",
+ "Application": "Applikazzjoni",
+ "Artists": "Artisti",
+ "AuthenticationSucceededWithUserName": "{1} awtentikat b'suċċess",
+ "Books": "Kotba",
+ "CameraImageUploadedFrom": "Ttellgħet immaġni ġdida tal-kamera minn {1}",
+ "Channels": "Kanali",
+ "ChapterNameValue": "Kapitlu {0}",
+ "Collections": "Kollezzjonijiet",
+ "DeviceOfflineWithName": "{0} inqatgħa",
+ "DeviceOnlineWithName": "{0} qabad",
+ "External": "Estern",
+ "FailedLoginAttemptWithUserName": "Tentattiv t'aċċess fallut minn {0}",
+ "Favorites": "Favoriti",
+ "Forced": "Sfurzat",
+ "Genres": "Ġeneri",
+ "HeaderAlbumArtists": "Artisti tal-album",
+ "HeaderContinueWatching": "Kompli Segwi",
+ "HeaderFavoriteAlbums": "Albums Favoriti",
+ "HeaderFavoriteArtists": "Artisti Favoriti",
+ "HeaderFavoriteEpisodes": "Episodji Favoriti",
+ "HeaderFavoriteShows": "Programmi Favoriti",
+ "HeaderFavoriteSongs": "Kanzunetti Favoriti",
+ "HeaderNextUp": "Li Jmiss",
+ "SubtitleDownloadFailureFromForItem": "Is-sottotitli naqsu milli jitniżżlu minn {0} għal {1}",
+ "UserPasswordChangedWithName": "Il-password inbidel għall-utent {0}",
+ "TaskUpdatePluginsDescription": "Iniżżel u jinstalla aġġornamenti għal plugins li huma kkonfigurati biex jaġġornaw awtomatikament.",
+ "TaskDownloadMissingSubtitlesDescription": "Ifittex fuq l-internet għal sottotitli neqsin abbażi tal-konfigurazzjoni tal-metadata.",
+ "TaskOptimizeDatabaseDescription": "Jikkompatti d-database u jaqta' l-ispazju ħieles. It-tħaddim ta' dan il-kompitu wara li tiskennja l-librerija jew tagħmel bidliet oħra li jimplikaw modifiki fid-database jistgħu jtejbu l-prestazzjoni.",
+ "Default": "Standard",
+ "Folders": "Folders",
+ "HeaderLiveTV": "TV Dirett",
+ "HeaderRecordingGroups": "Gruppi ta' Reġistrazzjoni",
+ "HearingImpaired": "Nuqqas ta' Smigħ",
+ "HomeVideos": "Vidjows Personali",
+ "Inherit": "Jiret",
+ "ItemAddedWithName": "{0} ġie miżjud mal-librerija",
+ "ItemRemovedWithName": "{0} tneħħa mil-librerija",
+ "LabelIpAddressValue": "Indirizz IP: {0}",
+ "Latest": "Tal-Aħħar",
+ "MessageApplicationUpdated": "Jellyfin Server ġie aġġornat",
+ "MessageApplicationUpdatedTo": "JellyFin Server ġie aġġornat għal {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Is-sezzjoni {0} tal-konfigurazzjoni tas-server ġiet aġġornata",
+ "MessageServerConfigurationUpdated": "Il-konfigurazzjoni tas-server ġiet aġġornata",
+ "MixedContent": "Kontenut imħallat",
+ "Movies": "Films",
+ "Music": "Mużika",
+ "MusicVideos": "Vidjows tal-Mużika",
+ "NameInstallFailed": "L-installazzjoni ta' {0} falliet",
+ "NameSeasonNumber": "Staġun {0}",
+ "NameSeasonUnknown": "Staġun Mhux Magħruf",
+ "NewVersionIsAvailable": "Verżjoni ġdida ta' Jellyfin Server hija disponibbli biex titniżżel.",
+ "NotificationOptionApplicationUpdateAvailable": "Aġġornament tal-applikazzjoni disponibbli",
+ "NotificationOptionCameraImageUploaded": "Immaġini tal-kamera mtella'",
+ "LabelRunningTimeValue": "Tul: {0}",
+ "NotificationOptionApplicationUpdateInstalled": "Aġġornament tal-applikazzjoni ġie installat",
+ "NotificationOptionAudioPlayback": "Il-playback tal-awdjo beda",
+ "NotificationOptionAudioPlaybackStopped": "Il-playback tal-awdjo twaqqaf",
+ "NotificationOptionInstallationFailed": "Installazzjoni falliet",
+ "NotificationOptionNewLibraryContent": "Kontenut ġdid miżjud",
+ "NotificationOptionPluginError": "Ħsara fil-plugin",
+ "NotificationOptionPluginInstalled": "Plugin installat",
+ "NotificationOptionPluginUninstalled": "Plugin tneħħa",
+ "NotificationOptionServerRestartRequired": "Meħtieġ l-istartjar mill-ġdid tas-server",
+ "NotificationOptionTaskFailed": "Falliment tal-kompitu skedat",
+ "NotificationOptionUserLockedOut": "Utent imsakkar",
+ "Photos": "Ritratti",
+ "Playlists": "Playlists",
+ "Plugin": "Plugin",
+ "PluginInstalledWithName": "{0} ġie installat",
+ "PluginUninstalledWithName": "{0} ġie mneħħi",
+ "PluginUpdatedWithName": "{0} ġie aġġornat",
+ "ProviderValue": "Fornitur: {0}",
+ "ScheduledTaskFailedWithName": "{0} falla",
+ "ScheduledTaskStartedWithName": "{0} beda",
+ "ServerNameNeedsToBeRestarted": "{0} jeħtieġ li jerġa' jinbeda",
+ "Songs": "Kanzunetti",
+ "StartupEmbyServerIsLoading": "Jellyfin Server qed jixgħel. Jekk jogħġbok erġa' pprova dalwaqt.",
+ "Sync": "Sinkronizza",
+ "System": "Sistema",
+ "Undefined": "Mhux Definit",
+ "User": "Utent",
+ "UserCreatedWithName": "L-utent {0} inħoloq",
+ "UserDeletedWithName": "L-utent {0} tħassar",
+ "UserDownloadingItemWithValues": "{0} qed iniżżel {1}",
+ "UserLockedOutWithName": "L-utent {0} ġie msakkar",
+ "UserOfflineFromDevice": "{0} skonnettja minn {1}",
+ "UserOnlineFromDevice": "{0} huwa online minn {1}",
+ "NotificationOptionPluginUpdateInstalled": "Aġġornament ta' plugin ġie installat",
+ "NotificationOptionVideoPlayback": "Il-playback tal-vidjow beda",
+ "NotificationOptionVideoPlaybackStopped": "Il-playback tal-vidjow waqaf",
+ "Shows": "Programmi",
+ "TvShows": "Programmi tat-TV",
+ "UserPolicyUpdatedWithName": "Il-policy tal-utent ġiet aġġornata għal {0}",
+ "UserStartedPlayingItemWithValues": "{0} qed iħaddem {1} fuq {2}",
+ "UserStoppedPlayingItemWithValues": "{0} waqaf iħaddem {1} fuq {2}",
+ "ValueHasBeenAddedToLibrary": "{0} ġie miżjud mal-librerija tal-midja tiegħek",
+ "ValueSpecialEpisodeName": "Speċjali - {0}",
+ "VersionNumber": "Verżjoni {0}",
+ "TasksMaintenanceCategory": "Manutenzjoni",
+ "TasksLibraryCategory": "Librerija",
+ "TasksApplicationCategory": "Applikazzjoni",
+ "TasksChannelsCategory": "Kanali tal-Internet",
+ "TaskCleanActivityLog": "Naddaf il-Logg tal-Attività",
+ "TaskCleanActivityLogDescription": "Iħassar l-entrati tar-reġistru tal-attività eqdem mill-età kkonfigurata.",
+ "TaskCleanCache": "Naddaf id-Direttorju tal-Cache",
+ "TaskCleanCacheDescription": "Iħassar il-fajls tal-cache li m'għadhomx meħtieġa mis-sistema.",
+ "TaskRefreshChapterImages": "Oħroġ l-Immaġini tal-Kapitolu",
+ "TaskRefreshChapterImagesDescription": "Joħloq thumbnails għal vidjows li għandhom kapitli.",
+ "TaskAudioNormalization": "Normalizzazzjoni Awdjo",
+ "TaskAudioNormalizationDescription": "Skennja fajls għal data ta' normalizzazzjoni awdjo.",
+ "TaskRefreshLibrary": "Skennja l-Librerija tal-Midja",
+ "TaskRefreshLibraryDescription": "Jiskennja l-librerija tal-midja tiegħek għal fajls ġodda u jġedded il-metadejta.",
+ "TaskCleanLogs": "Naddaf id-Direttorju tal-Logg",
+ "TaskCleanLogsDescription": "Iħassar fajls tal-logg eqdem minn {0} ijiem.",
+ "TaskRefreshPeople": "Aġġorna Persuni",
+ "TaskRefreshPeopleDescription": "Jaġġorna l-metadejta għall-atturi u d-diretturi fil-librerija tal-midja tiegħek.",
+ "TaskRefreshTrickplayImages": "Iġġenera Stampi Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Joħloq previews trickplay għal vidjows fil-libreriji attivati.",
+ "TaskUpdatePlugins": "Aġġorna il-Plugins",
+ "TaskCleanTranscode": "Naddaf id-Direttorju tat-Transcode",
+ "TaskCleanTranscodeDescription": "Iħassar fajls transcode eqdem minn ġurnata.",
+ "TaskRefreshChannels": "Aġġorna l-Kanali",
+ "TaskRefreshChannelsDescription": "Aġġorna l-informazzjoni tal-kanali tal-internet.",
+ "TaskDownloadMissingSubtitles": "Niżżel is-sottotitli nieqsa",
+ "TaskOptimizeDatabase": "Ottimizza d-database",
+ "TaskKeyframeExtractor": "Estrattur ta' Keyframes",
+ "TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-vidjow biex joħloq playlists HLS aktar preċiżi. Dan il-kompitu jista' jdum għal żmien twil.",
+ "TaskCleanCollectionsAndPlaylists": "Naddaf il-kollezzjonijiet u l-playlists",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu."
+}
diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json
index 198f7540c8..4cb4cdc757 100644
--- a/Emby.Server.Implementations/Localization/Core/my.json
+++ b/Emby.Server.Implementations/Localization/Core/my.json
@@ -48,7 +48,7 @@
"Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
"System": "စနစ်",
- "Sync": "ထပ်တူကျသည်။",
+ "Sync": "ချိန်ကိုက်မည်",
"SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
"Songs": "သီချင်းများ",
@@ -104,7 +104,7 @@
"HeaderFavoriteSongs": "အကြိုက်ဆုံးသီချင်းများ",
"HeaderFavoriteShows": "အကြိုက်ဆုံး ဇာတ်လမ်းတွဲများ",
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
- "HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
+ "HeaderFavoriteArtists": "အကြိုက်ဆုံး အနုပညာရှင်များ",
"HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
"HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
"HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
@@ -120,5 +120,11 @@
"AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
"Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
- "External": "ပြင်ပ"
+ "External": "ပြင်ပ",
+ "TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
+ "TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။",
+ "TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
+ "TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
+ "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
+ "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index b6c15d871a..b66818ddca 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -126,5 +126,9 @@
"External": "Ekstern",
"HearingImpaired": "Hørselshemmet",
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
- "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker."
+ "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.",
+ "TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
+ "TaskAudioNormalization": "Lyd Normalisering",
+ "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index a925b71345..4f076b6805 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -11,7 +11,7 @@
"Collections": "Collecties",
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
"DeviceOnlineWithName": "{0} is verbonden",
- "FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
+ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
@@ -124,7 +124,11 @@
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
"TaskKeyframeExtractor": "Keyframes uitpakken",
"External": "Extern",
- "HearingImpaired": "Slechthorend",
+ "HearingImpaired": "Slechthorenden",
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
- "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld."
+ "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
+ "TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
+ "TaskAudioNormalization": "Geluidsnormalisatie",
+ "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index d0c914de37..ff6376258e 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -118,5 +118,6 @@
"Undefined": "Udefinert",
"Forced": "Tvungen",
"Default": "Standard",
- "External": "Ekstern"
+ "External": "Ekstern",
+ "HearingImpaired": "Nedsett høyrsel"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index 1f982feaf4..a25099ee0b 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -104,7 +104,7 @@
"Forced": "ਮਜਬੂਰ",
"Folders": "ਫੋਲਡਰ",
"Favorites": "ਮਨਪਸੰਦ",
- "FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ",
+ "FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ",
"DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
"DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
"Default": "ਡਿਫੌਲਟ",
@@ -119,5 +119,6 @@
"AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}",
"Albums": "ਐਲਬਮਾਂ",
"TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ",
- "External": "ਬਾਹਰੀ"
+ "External": "ਬਾਹਰੀ",
+ "HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index bd572b744b..f36385be20 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -11,7 +11,7 @@
"Collections": "Kolekcje",
"DeviceOfflineWithName": "{0} został rozłączony",
"DeviceOnlineWithName": "{0} połączył się",
- "FailedLoginAttemptWithUserName": "Próba logowania przez {0} zakończona niepowodzeniem",
+ "FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
"Favorites": "Ulubione",
"Folders": "Foldery",
"Genres": "Gatunki",
@@ -98,8 +98,8 @@
"TaskRefreshChannels": "Odśwież kanały",
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
"TaskCleanTranscode": "Wyczyść folder transkodowania",
- "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
- "TaskUpdatePlugins": "Aktualizuj pluginy",
+ "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje wtyczek, które są skonfigurowane do automatycznej aktualizacji.",
+ "TaskUpdatePlugins": "Aktualizuj wtyczki",
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
"TaskRefreshPeople": "Odśwież obsadę",
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
@@ -126,5 +126,9 @@
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
"HearingImpaired": "Niedosłyszący",
"TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
- "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
+ "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
+ "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
+ "TaskAudioNormalization": "Normalizacja dźwięku",
+ "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 2c8c46050e..d9867f5e05 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -111,7 +111,7 @@
"TaskCleanCacheDescription": "Deletar arquivos temporários que não são mais necessários para o sistema.",
"TaskCleanCache": "Limpar Arquivos Temporários",
"TasksChannelsCategory": "Canais da Internet",
- "TasksApplicationCategory": "Aplicativo",
+ "TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskCleanActivityLogDescription": "Apaga o registro de atividades mais antigo que a idade configurada.",
@@ -126,5 +126,9 @@
"External": "Externo",
"HearingImpaired": "Deficiência Auditiva",
"TaskRefreshTrickplayImages": "Gerar imagens Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado."
+ "TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado.",
+ "TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
+ "TaskAudioNormalization": "Normalização de áudio",
+ "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 92ac2681e4..4f7ef3292f 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -126,5 +126,9 @@
"External": "Externo",
"HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de truques",
- "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas."
+ "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
+ "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
+ "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
+ "TaskAudioNormalization": "Normalização de áudio"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 103393a1e4..ff9a0d4f42 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
- "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
+ "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
+ "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
+ "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
+ "TaskAudioNormalization": "Normalização de áudio"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 537a6d3f2f..cd0120fc79 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -78,7 +78,7 @@
"Genres": "Genuri",
"Folders": "Dosare",
"Favorites": "Favorite",
- "FailedLoginAttemptWithUserName": "Încercare de conectare nereușită de la {0}",
+ "FailedLoginAttemptWithUserName": "Încercare de conectare eșuată pentru {0}",
"DeviceOnlineWithName": "{0} este conectat",
"DeviceOfflineWithName": "{0} s-a deconectat",
"Collections": "Colecții",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 26d678a0c3..3eb1e0468e 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -11,7 +11,7 @@
"Collections": "Коллекции",
"DeviceOfflineWithName": "{0} - отключено",
"DeviceOnlineWithName": "{0} - подключено",
- "FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
+ "FailedLoginAttemptWithUserName": "Неудачная попытка входа с {0}",
"Favorites": "Избранное",
"Folders": "Папки",
"Genres": "Жанры",
@@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
- "Latest": "Последние добавленные",
+ "Latest": "Последние",
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
@@ -126,5 +126,9 @@
"External": "Внешние",
"HearingImpaired": "Для слабослышащих",
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
+ "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
+ "TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
+ "TaskAudioNormalization": "Нормализация звука",
+ "TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 43594a42eb..a9b6fbeef4 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -126,5 +126,9 @@
"External": "Externé",
"HearingImpaired": "Sluchovo postihnutí",
"TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
+ "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.",
+ "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.",
+ "TaskAudioNormalization": "Normalizácia zvuku",
+ "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 1fc3cdbaaa..f40c4478a9 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -126,5 +126,9 @@
"External": "Extern",
"HearingImpaired": "Hörselskadad",
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
- "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek."
+ "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
+ "TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
+ "TaskAudioNormalization": "Ljudnormalisering",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
+ "TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 646d7d7a5f..7270d70fc4 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -125,5 +125,9 @@
"External": "வெளி",
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
- "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
+ "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
+ "TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
+ "TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
+ "TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
+ "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 3cdf743d55..da32e97762 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -123,5 +123,7 @@
"External": "ภายนอก",
"HearingImpaired": "บกพร่องทางการได้ยิน",
"TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม",
- "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน"
+ "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน",
+ "TaskRefreshTrickplayImages": "สร้างไฟล์รูปภาพสำหรับ Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index d7a627d127..1dceadc611 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -11,7 +11,7 @@
"Collections": "Koleksiyonlar",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
- "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
+ "FailedLoginAttemptWithUserName": "{0} kullanıcısının başarısız oturum açma girişimi",
"Favorites": "Favoriler",
"Folders": "Klasörler",
"Genres": "Türler",
@@ -126,5 +126,9 @@
"External": "Harici",
"HearingImpaired": "Duyma Engelli",
"TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur",
- "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur."
+ "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.",
+ "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin",
+ "TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.",
+ "TaskAudioNormalization": "Ses Normalleştirme"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 3f7fca427b..18073287b6 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -125,5 +125,9 @@
"External": "Зовнішній",
"HearingImpaired": "З порушеннями слуху",
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
- "TaskRefreshTrickplayImages": "Створити Trickplay-зображення"
+ "TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
+ "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
+ "TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
+ "TaskAudioNormalization": "Нормалізація аудіо"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json
index 43935f224b..a1b3035f37 100644
--- a/Emby.Server.Implementations/Localization/Core/uz.json
+++ b/Emby.Server.Implementations/Localization/Core/uz.json
@@ -8,5 +8,20 @@
"Channels": "Kanallar",
"Books": "Kitoblar",
"Artists": "Ijrochilar",
- "Albums": "Albomlar"
+ "Albums": "Albomlar",
+ "AuthenticationSucceededWithUserName": "{0} muvaffaqiyatli tasdiqlandi",
+ "AppDeviceValues": "Ilova: {0}, Qurilma: {1}",
+ "Application": "Ilova",
+ "CameraImageUploadedFrom": "{0}dan yangi kamera rasmi yuklandi",
+ "DeviceOnlineWithName": "{0} ulangan",
+ "ItemRemovedWithName": "{0} kutbxonadan o'chirildi",
+ "External": "Tashqi",
+ "FailedLoginAttemptWithUserName": "Muvafaqiyatsiz kirishlar soni {0}",
+ "Forced": "Majburiy",
+ "ChapterNameValue": "{0}chi bo'lim",
+ "DeviceOfflineWithName": "{0} aloqa uzildi",
+ "HeaderLiveTV": "Jonli TV",
+ "HeaderNextUp": "Keyingisi",
+ "ItemAddedWithName": "{0} kutbxonaga qo'shildi",
+ "LabelIpAddressValue": "IP manzil: {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index e92752c5f7..4bedfe3b25 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -103,11 +103,11 @@
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
"HeaderFavoriteAlbums": "Album Ưa Thích",
- "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
+ "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
"DeviceOnlineWithName": "{0} đã kết nối",
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
"ChapterNameValue": "Phân Cảnh {0}",
- "Channels": "Các Kênh",
+ "Channels": "Kênh",
"CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
"Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
@@ -125,5 +125,9 @@
"External": "Bên ngoài",
"HearingImpaired": "Khiếm Thính",
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật."
+ "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
+ "TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.",
+ "TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
+ "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh."
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index b88d4eeaf5..808f737935 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -11,7 +11,7 @@
"Collections": "合集",
"DeviceOfflineWithName": "{0} 已断开",
"DeviceOnlineWithName": "{0} 已连接",
- "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
+ "FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败",
"Favorites": "我的最爱",
"Folders": "文件夹",
"Genres": "类型",
@@ -126,5 +126,9 @@
"External": "外部",
"HearingImpaired": "听力障碍",
"TaskRefreshTrickplayImages": "生成时间轴缩略图",
- "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
+ "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。",
+ "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
+ "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
+ "TaskAudioNormalization": "音频标准化",
+ "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index d57a2811d1..f06bbc5912 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -1,25 +1,25 @@
{
"Albums": "專輯",
- "AppDeviceValues": "App:{0},裝置:{1}",
+ "AppDeviceValues": "應用程式:{0},裝置:{1}",
"Application": "應用程式",
- "Artists": "演出者",
- "AuthenticationSucceededWithUserName": "{0} 成功授權",
- "Books": "圖書",
- "CameraImageUploadedFrom": "{0} 已經成功上傳一張相片",
+ "Artists": "藝人",
+ "AuthenticationSucceededWithUserName": "成功授權 {0}",
+ "Books": "書籍",
+ "CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
- "Collections": "合輯",
- "DeviceOfflineWithName": "{0} 已經斷線",
- "DeviceOnlineWithName": "{0} 已經連線",
- "FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
+ "Collections": "系列",
+ "DeviceOfflineWithName": "{0} 已中斷連接",
+ "DeviceOnlineWithName": "{0} 已連接",
+ "FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
- "HeaderContinueWatching": "繼續觀賞",
+ "HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
- "HeaderFavoriteArtists": "最愛演出者",
- "HeaderFavoriteEpisodes": "最愛影集",
+ "HeaderFavoriteArtists": "最愛藝人",
+ "HeaderFavoriteEpisodes": "最愛劇集",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
@@ -30,8 +30,8 @@
"LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin Server 已經更新",
- "MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
+ "MessageApplicationUpdated": "Jellyfin 伺服器已經更新",
+ "MessageApplicationUpdatedTo": "Jellyfin 伺服器已經更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "混合內容",
@@ -41,7 +41,7 @@
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
- "NewVersionIsAvailable": "新版本的 Jellyfin Server 已經可供下載。",
+ "NewVersionIsAvailable": "新版本的 Jellyfin 伺服器已經可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式更新已安裝",
"NotificationOptionAudioPlayback": "音訊播放已開始",
@@ -49,52 +49,52 @@
"NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
- "NotificationOptionPluginError": "附加元件安裝失敗",
- "NotificationOptionPluginInstalled": "附加元件已安裝",
- "NotificationOptionPluginUninstalled": "附加元件已移除",
- "NotificationOptionPluginUpdateInstalled": "附加元件已更新",
+ "NotificationOptionPluginError": "擴充功能錯誤",
+ "NotificationOptionPluginInstalled": "擴充功能已安裝",
+ "NotificationOptionPluginUninstalled": "擴充功能已移除",
+ "NotificationOptionPluginUpdateInstalled": "擴充功能已更新",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
- "NotificationOptionTaskFailed": "排程任務失敗",
+ "NotificationOptionTaskFailed": "擴充功能任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
"NotificationOptionVideoPlayback": "影片播放已開始",
"NotificationOptionVideoPlaybackStopped": "影片播放已停止",
"Photos": "相片",
"Playlists": "播放清單",
- "Plugin": "附加元件",
- "PluginInstalledWithName": "{0} 已安裝",
- "PluginUninstalledWithName": "{0} 已移除",
- "PluginUpdatedWithName": "{0} 已更新",
- "ProviderValue": "提供商: {0}",
- "ScheduledTaskFailedWithName": "排程任務 {0} 已失敗",
+ "Plugin": "擴充功能",
+ "PluginInstalledWithName": "已安裝 {0}",
+ "PluginUninstalledWithName": "已移除 {0}",
+ "PluginUpdatedWithName": "已更新 {0}",
+ "ProviderValue": "提供者:{0}",
+ "ScheduledTaskFailedWithName": "排程任務 {0} 執行失敗",
"ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
"ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
- "StartupEmbyServerIsLoading": "Jellyfin Server 載入中,請稍後再試。",
+ "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
"User": "使用者",
- "UserCreatedWithName": "使用者 {0} 已建立",
- "UserDeletedWithName": "使用者 {0} 已移除",
+ "UserCreatedWithName": "已建立使用者 {0}",
+ "UserDeletedWithName": "已刪除使用者 {0}",
"UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
- "UserLockedOutWithName": "使用者 {0} 已鎖定",
+ "UserLockedOutWithName": "使用者 {0} 已被鎖定",
"UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
"UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
- "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
- "UserStartedPlayingItemWithValues": "{0}正在 {2} 上播放 {1}",
+ "UserPolicyUpdatedWithName": "使用者權限已更新為 {0}",
+ "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
"ValueSpecialEpisodeName": "特輯 - {0}",
"VersionNumber": "版本 {0}",
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
- "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
+ "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"TaskDownloadMissingSubtitlesDescription": "透過媒體資訊從網路上搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
- "TaskUpdatePlugins": "更新附加元件",
+ "TaskUpdatePlugins": "更新擴充功能",
"TaskRefreshPeople": "更新人物",
"TaskCleanLogsDescription": "刪除超過 {0} 天的日誌文件。",
"TaskCleanLogs": "清空日誌資料夾",
@@ -105,9 +105,9 @@
"TaskCleanCache": "清除快取資料夾",
"TasksLibraryCategory": "媒體庫",
"TaskRefreshChannelsDescription": "重新整理網路頻道資料。",
- "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
- "TaskCleanTranscode": "清除轉碼資料夾",
- "TaskUpdatePluginsDescription": "為已設置為自動更新的附加元件下載並安裝更新。",
+ "TaskCleanTranscodeDescription": "刪除超過一天的轉檔。",
+ "TaskCleanTranscode": "清除轉檔資料夾",
+ "TaskUpdatePluginsDescription": "下載並更新已啟用自動更新的擴充功能。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的資訊。",
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網路頻道",
@@ -125,5 +125,9 @@
"External": "外部",
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "生成快轉縮圖",
- "TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。"
+ "TaskRefreshTrickplayImagesDescription": "為啟用快轉縮圖的媒體庫生成快轉縮圖。",
+ "TaskCleanCollectionsAndPlaylists": "清理系列和播放清單",
+ "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。",
+ "TaskAudioNormalization": "音量標準化",
+ "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 16776b6bd6..ac453a5b09 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -278,6 +278,13 @@ namespace Emby.Server.Implementations.Localization
return null;
}
+ // Convert integers directly
+ // This may override some of the locale specific age ratings (but those always map to the same age)
+ if (int.TryParse(rating, out var ratingAge))
+ {
+ return ratingAge;
+ }
+
// Fairly common for some users to have "Rated R" in their rating field
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -314,7 +321,11 @@ namespace Emby.Server.Implementations.Localization
// Try splitting by : to handle "Germany: FSK-18"
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"
@@ -325,8 +336,12 @@ namespace Emby.Server.Implementations.Localization
// Extract culture from country prefix
var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
- // Check rating system of culture
- return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
+ var ratingLevelRightPart = ratingSpan.RightPart('-');
+ if (ratingLevelRightPart.Length != 0)
+ {
+ // Check rating system of culture
+ return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
+ }
}
return null;
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
index 6881259172..6e12759a46 100644
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/au.csv
@@ -1,11 +1,11 @@
Exempt,0
G,0
7+,7
+PG,15
M,15
MA,15
MA15+,15
MA 15+,15
-PG,16
16+,16
R,18
R18+,18
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index aea8d65322..47ff22c0b3 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -59,68 +59,74 @@ namespace Emby.Server.Implementations.Playlists
_appConfig = appConfig;
}
+ public Playlist GetPlaylistForUser(Guid playlistId, Guid userId)
+ {
+ return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault();
+ }
+
public IEnumerable GetPlaylists(Guid userId)
{
var user = _userManager.GetUserById(userId);
-
- return GetPlaylistsFolder(userId).GetChildren(user, true).OfType();
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Playlist],
+ Recursive = true,
+ DtoOptions = new DtoOptions(false)
+ })
+ .Cast()
+ .Where(p => p.IsVisible(user));
}
- public async Task CreatePlaylist(PlaylistCreationRequest options)
+ public async Task CreatePlaylist(PlaylistCreationRequest request)
{
- var name = options.Name;
+ var name = request.Name;
var folderName = _fileSystem.GetValidFilename(name);
- var parentFolder = GetPlaylistsFolder(options.UserId);
+ var parentFolder = GetPlaylistsFolder(request.UserId);
if (parentFolder is null)
{
throw new ArgumentException(nameof(parentFolder));
}
- if (options.MediaType is null || options.MediaType == MediaType.Unknown)
+ if (request.MediaType is null || request.MediaType == MediaType.Unknown)
{
- foreach (var itemId in options.ItemIdList)
+ foreach (var itemId in request.ItemIdList)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- throw new ArgumentException("No item exists with the supplied Id");
- }
-
+ var item = _libraryManager.GetItemById(itemId) ?? throw new ArgumentException("No item exists with the supplied Id");
if (item.MediaType != MediaType.Unknown)
{
- options.MediaType = item.MediaType;
+ request.MediaType = item.MediaType;
}
else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre)
{
- options.MediaType = MediaType.Audio;
+ request.MediaType = MediaType.Audio;
}
else if (item is Genre)
{
- options.MediaType = MediaType.Video;
+ request.MediaType = MediaType.Video;
}
else
{
if (item is Folder folder)
{
- options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
+ request.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
.Select(i => i.MediaType)
.FirstOrDefault(i => i != MediaType.Unknown);
}
}
- if (options.MediaType is not null && options.MediaType != MediaType.Unknown)
+ if (request.MediaType is not null && request.MediaType != MediaType.Unknown)
{
break;
}
}
}
- if (options.MediaType is null || options.MediaType == MediaType.Unknown)
+ if (request.MediaType is null || request.MediaType == MediaType.Unknown)
{
- options.MediaType = MediaType.Audio;
+ request.MediaType = MediaType.Audio;
}
- var user = _userManager.GetUserById(options.UserId);
+ var user = _userManager.GetUserById(request.UserId);
var path = Path.Combine(parentFolder.Path, folderName);
path = GetTargetPath(path);
@@ -133,19 +139,20 @@ namespace Emby.Server.Implementations.Playlists
{
Name = name,
Path = path,
- OwnerUserId = options.UserId,
- Shares = options.Shares ?? Array.Empty()
+ OwnerUserId = request.UserId,
+ Shares = request.Users ?? [],
+ OpenAccess = request.Public ?? false
};
- playlist.SetMediaType(options.MediaType);
+ playlist.SetMediaType(request.MediaType);
parentFolder.AddChild(playlist);
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false);
- if (options.ItemIdList.Count > 0)
+ if (request.ItemIdList.Count > 0)
{
- await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
+ await AddToPlaylistInternal(playlist.Id, request.ItemIdList, user, new DtoOptions(false)
{
EnableImages = true
}).ConfigureAwait(false);
@@ -160,7 +167,19 @@ namespace Emby.Server.Implementations.Playlists
}
}
- private string GetTargetPath(string path)
+ private List GetUserPlaylists(Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ var playlistsFolder = GetPlaylistsFolder(userId);
+ if (playlistsFolder is null)
+ {
+ return [];
+ }
+
+ return playlistsFolder.GetChildren(user, true).OfType().ToList();
+ }
+
+ private static string GetTargetPath(string path)
{
while (Directory.Exists(path))
{
@@ -170,14 +189,14 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private List GetPlaylistItems(IEnumerable itemIds, MediaType playlistMediaType, User user, DtoOptions options)
+ private IReadOnlyList GetPlaylistItems(IEnumerable itemIds, User user, DtoOptions options)
{
- var items = itemIds.Select(i => _libraryManager.GetItemById(i)).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 AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId)
+ public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId)
{
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
@@ -194,7 +213,7 @@ namespace Emby.Server.Implementations.Playlists
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
// 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);
// Filter out duplicate items, if necessary
@@ -224,20 +243,10 @@ namespace Emby.Server.Implementations.Playlists
return;
}
- // Create a new array with the updated playlist items
- var newLinkedChildren = new LinkedChild[playlist.LinkedChildren.Length + childrenToAdd.Count];
- playlist.LinkedChildren.CopyTo(newLinkedChildren, 0);
- childrenToAdd.CopyTo(newLinkedChildren, playlist.LinkedChildren.Length);
-
// Update the playlist in the repository
- playlist.LinkedChildren = newLinkedChildren;
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
- // Update the playlist on disk
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
// Refresh playlist metadata
_providerManager.QueueRefresh(
@@ -249,7 +258,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable entryIds)
+ public async Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable entryIds)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
@@ -266,12 +275,7 @@ namespace Emby.Server.Implementations.Playlists
.Select(i => i.Item1)
.ToArray();
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
_providerManager.QueueRefresh(
playlist.Id,
@@ -313,14 +317,9 @@ namespace Emby.Server.Implementations.Playlists
newList.Insert(newIndex, item);
}
- playlist.LinkedChildren = newList.ToArray();
+ playlist.LinkedChildren = [.. newList];
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
///
@@ -430,8 +429,11 @@ namespace Emby.Server.Implementations.Playlists
}
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
- var playlist = new M3uPlaylist();
- playlist.IsExtended = true;
+ var playlist = new M3uPlaylist
+ {
+ IsExtended = true
+ };
+
foreach (var child in item.GetLinkedChildren())
{
var entry = new M3uPlaylistEntry()
@@ -481,7 +483,7 @@ namespace Emby.Server.Implementations.Playlists
}
}
- private string NormalizeItemPath(string playlistPath, string itemPath)
+ private static string NormalizeItemPath(string playlistPath, string itemPath)
{
return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath);
}
@@ -516,11 +518,13 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
+ ///
public Folder GetPlaylistsFolder()
{
return GetPlaylistsFolder(Guid.Empty);
}
+ ///
public Folder GetPlaylistsFolder(Guid userId)
{
const string TypeName = "PlaylistsFolder";
@@ -532,21 +536,16 @@ namespace Emby.Server.Implementations.Playlists
///
public async Task RemovePlaylistsAsync(Guid userId)
{
- var playlists = GetPlaylists(userId);
+ var playlists = GetUserPlaylists(userId);
foreach (var playlist in playlists)
{
// Update owner if shared
- var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray();
- if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid))
+ var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToList();
+ if (rankedShares.Count > 0)
{
- playlist.OwnerUserId = guid;
+ playlist.OwnerUserId = rankedShares[0].UserId;
playlist.Shares = rankedShares.Skip(1).ToArray();
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
else if (!playlist.OpenAccess)
{
@@ -563,5 +562,76 @@ namespace Emby.Server.Implementations.Playlists
}
}
}
+
+ public async Task UpdatePlaylist(PlaylistUpdateRequest request)
+ {
+ var playlist = GetPlaylistForUser(request.Id, request.UserId);
+
+ if (request.Ids is not null)
+ {
+ playlist.LinkedChildren = [];
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+
+ var user = _userManager.GetUserById(request.UserId);
+ await AddToPlaylistInternal(request.Id, request.Ids, user, new DtoOptions(false)
+ {
+ EnableImages = true
+ }).ConfigureAwait(false);
+
+ playlist = GetPlaylistForUser(request.Id, request.UserId);
+ }
+
+ if (request.Name is not null)
+ {
+ playlist.Name = request.Name;
+ }
+
+ if (request.Users is not null)
+ {
+ playlist.Shares = request.Users;
+ }
+
+ if (request.Public is not null)
+ {
+ playlist.OpenAccess = request.Public.Value;
+ }
+
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ public async Task AddUserToShares(PlaylistUserUpdateRequest request)
+ {
+ var userId = request.UserId;
+ var playlist = GetPlaylistForUser(request.Id, userId);
+ var shares = playlist.Shares.ToList();
+ var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ if (existingUserShare is not null)
+ {
+ shares.Remove(existingUserShare);
+ }
+
+ shares.Add(new PlaylistUserPermissions(userId, request.CanEdit ?? false));
+ playlist.Shares = shares;
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ public async Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share)
+ {
+ var playlist = GetPlaylistForUser(playlistId, userId);
+ var shares = playlist.Shares.ToList();
+ shares.Remove(share);
+ playlist.Shares = shares;
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ private async Task UpdatePlaylistInternal(Playlist playlist)
+ {
+ await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+
+ if (playlist.IsFile)
+ {
+ SavePlaylistFile(playlist);
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index efb6436ae9..40e1bbf159 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -256,8 +256,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
get
{
- var triggers = InternalTriggers;
- return triggers.Select(i => i.Item1).ToArray();
+ return Array.ConvertAll(InternalTriggers, i => i.Item1);
}
set
@@ -269,7 +268,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
SaveTriggers(triggerList);
- InternalTriggers = triggerList.Select(i => new Tuple(i, GetTrigger(i))).ToArray();
+ InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple(i, GetTrigger(i)));
}
}
@@ -503,7 +502,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
private Tuple[] LoadTriggers()
{
// This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
- var settings = LoadTriggerSettings().Where(i => i is not null).ToArray();
+ var settings = LoadTriggerSettings().Where(i => i is not null);
return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
new file mode 100644
index 0000000000..301c049154
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+///
+/// The audio normalization task.
+///
+public partial class AudioNormalizationTask : IScheduledTask
+{
+ private readonly IItemRepository _itemRepository;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IApplicationPaths _applicationPaths;
+ private readonly ILocalizationManager _localization;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public AudioNormalizationTask(
+ IItemRepository itemRepository,
+ ILibraryManager libraryManager,
+ IMediaEncoder mediaEncoder,
+ IApplicationPaths applicationPaths,
+ ILocalizationManager localizationManager,
+ ILogger logger)
+ {
+ _itemRepository = itemRepository;
+ _libraryManager = libraryManager;
+ _mediaEncoder = mediaEncoder;
+ _applicationPaths = applicationPaths;
+ _localization = localizationManager;
+ _logger = logger;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskAudioNormalization");
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ ///
+ public string Key => "AudioNormalization";
+
+ [GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
+ private static partial Regex LUFSRegex();
+
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ foreach (var library in _libraryManager.RootFolder.Children)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(library);
+ if (!libraryOptions.EnableLUFSScan)
+ {
+ continue;
+ }
+
+ // Album gain
+ var albums = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.MusicAlbum],
+ Parent = library,
+ Recursive = true
+ });
+
+ foreach (var a in albums)
+ {
+ if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
+ {
+ continue;
+ }
+
+ // Skip albums that don't have multiple tracks, album gain is useless here
+ var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
+ if (albumTracks.Count <= 1)
+ {
+ continue;
+ }
+
+ _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
+ var tempDir = _applicationPaths.TempDirectory;
+ Directory.CreateDirectory(tempDir);
+ var tempFile = Path.Join(tempDir, a.Id + ".concat");
+ var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
+ await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ a.LUFS = await CalculateLUFSAsync(
+ string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
+ cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ _itemRepository.SaveItems(albums, cancellationToken);
+
+ // Track gain
+ var tracks = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Audio],
+ IncludeItemTypes = [BaseItemKind.Audio],
+ Parent = library,
+ Recursive = true
+ });
+
+ foreach (var t in tracks)
+ {
+ if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
+ {
+ continue;
+ }
+
+ t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken);
+ }
+
+ _itemRepository.SaveItems(tracks, cancellationToken);
+ }
+ }
+
+ ///
+ public IEnumerable GetDefaultTriggers()
+ {
+ return
+ [
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerInterval,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ }
+ ];
+ }
+
+ private async Task CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
+ {
+ var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
+
+ using (var process = new Process()
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = args,
+ RedirectStandardOutput = false,
+ RedirectStandardError = true
+ },
+ })
+ {
+ try
+ {
+ _logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
+ return null;
+ }
+
+ using var reader = process.StandardError;
+ await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
+ {
+ 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");
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index d03d408633..36456504be 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 812df81922..8040972192 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -116,7 +116,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
foreach (var linkedChild in folder.LinkedChildren)
{
var path = linkedChild.Path;
- if (!File.Exists(path))
+ if (!File.Exists(path) && !Directory.Exists(path))
{
_logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
(itemsToRemove ??= new List()).Add(linkedChild);
@@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
{
_logger.LogDebug("Updating {FolderName}", folder.Name);
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
+ _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
-
- _providerManager.QueueRefresh(
- folder.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- },
- RefreshPriority.High);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 03935b384c..fc3ad90f6c 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
@@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
cancellationToken.ThrowIfCancellationRequested();
- DeleteFile(file.FullName);
+ FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
index++;
}
- DeleteEmptyFolders(directory);
+ FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
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);
- }
- }
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index e4e565c642..254500ccd9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
@@ -62,14 +62,15 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
///
public bool IsLogged => true;
- ///
- /// Creates the triggers that define when the task will run.
- ///
- /// IEnumerable{BaseTaskTrigger}.
+ ///
public IEnumerable GetDefaultTriggers()
{
return new[]
{
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerStartup
+ },
new TaskTriggerInfo
{
Type = TaskTriggerInfo.TriggerInterval,
@@ -113,53 +114,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
cancellationToken.ThrowIfCancellationRequested();
- DeleteFile(file.FullName);
+ FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
index++;
}
- DeleteEmptyFolders(directory);
+ FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
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);
- }
- }
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index d65ac2e5ea..9425b47d02 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -27,45 +27,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
TaskOptions = taskOptions;
}
- ///
- /// Occurs when [triggered].
- ///
+ ///
public event EventHandler? Triggered;
- ///
- /// Gets the options of this task.
- ///
+ ///
public TaskOptions TaskOptions { get; }
- ///
- /// Stars waiting for the trigger action.
- ///
- /// The last result.
- /// The logger.
- /// The name of the task.
- /// if set to true [is application startup].
+ ///
public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup)
{
DisposeTimer();
+ DateTime now = DateTime.UtcNow;
DateTime triggerDate;
if (lastResult is null)
{
// Task has never been completed before
- triggerDate = DateTime.UtcNow.AddHours(1);
+ triggerDate = now.AddHours(1);
}
else
{
- triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval);
- }
-
- if (DateTime.UtcNow > triggerDate)
- {
- triggerDate = DateTime.UtcNow.AddMinutes(1);
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate, now.AddMinutes(1) }.Max().Add(_interval);
}
- var dueTime = triggerDate - DateTime.UtcNow;
+ var dueTime = triggerDate - now;
var maxDueTime = TimeSpan.FromDays(7);
if (dueTime > maxDueTime)
@@ -76,9 +62,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
_timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
- ///
- /// Stops waiting for the trigger action.
- ///
+ ///
public void Stop()
{
DisposeTimer();
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 1bac2600ca..aa5fbbdf73 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -15,10 +15,9 @@ namespace Emby.Server.Implementations.Serialization
{
// Need to cache these
// http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
- private static readonly ConcurrentDictionary _serializers =
- new ConcurrentDictionary();
+ private readonly ConcurrentDictionary _serializers = new();
- private static XmlSerializer GetSerializer(Type type)
+ private XmlSerializer GetSerializer(Type type)
=> _serializers.GetOrAdd(
type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
static (_, t) => new XmlSerializer(t),
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 75945b08a2..3dda5fdee7 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -159,10 +159,7 @@ namespace Emby.Server.Implementations.Session
private void CheckDisposed()
{
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
+ ObjectDisposedException.ThrowIf(_disposed, this);
}
private void OnSessionStarted(SessionInfo info)
@@ -403,7 +400,7 @@ namespace Emby.Server.Implementations.Session
{
session.NowPlayingQueue = nowPlayingQueue;
- var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray();
+ var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
_libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
new DtoOptions(true));
@@ -1205,7 +1202,8 @@ namespace Emby.Server.Implementations.Session
new DtoOptions(false)
{
EnableImages = false
- })
+ },
+ user.DisplayMissingEpisodes)
.Where(i => !i.IsVirtualItem)
.SkipWhile(i => !i.Id.Equals(episode.Id))
.ToList();
@@ -1389,16 +1387,13 @@ namespace Emby.Server.Implementations.Session
if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
{
var user = _userManager.GetUserById(userId);
-
- var list = session.AdditionalUsers.ToList();
-
- list.Add(new SessionUserInfo
+ var newUser = new SessionUserInfo
{
UserId = userId,
UserName = user.Username
- });
+ };
- session.AdditionalUsers = list.ToArray();
+ session.AdditionalUsers = [..session.AdditionalUsers, newUser];
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index b3c93a904a..aba51de8f5 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -33,11 +33,6 @@ namespace Emby.Server.Implementations.Session
///
private const float ForceKeepAliveFactor = 0.75f;
- ///
- /// Lock used for accessing the KeepAlive cancellation token.
- ///
- private readonly object _keepAliveLock = new object();
-
///
/// The WebSocket watchlist.
///
@@ -55,7 +50,7 @@ namespace Emby.Server.Implementations.Session
///
/// The KeepAlive cancellation token.
///
- private CancellationTokenSource? _keepAliveCancellationToken;
+ private System.Timers.Timer _keepAlive;
///
/// Initializes a new instance of the class.
@@ -71,12 +66,34 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
+ _keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
+ {
+ AutoReset = true,
+ Enabled = false
+ };
+ _keepAlive.Elapsed += KeepAliveSockets;
}
///
public void Dispose()
{
- StopKeepAlive();
+ if (_keepAlive is not null)
+ {
+ _keepAlive.Stop();
+ _keepAlive.Elapsed -= KeepAliveSockets;
+ _keepAlive.Dispose();
+ _keepAlive = null!;
+ }
+
+ lock (_webSocketsLock)
+ {
+ foreach (var webSocket in _webSockets)
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ }
+
+ _webSockets.Clear();
+ }
}
///
@@ -164,7 +181,7 @@ namespace Emby.Server.Implementations.Session
webSocket.Closed += OnWebSocketClosed;
webSocket.LastKeepAliveDate = DateTime.UtcNow;
- StartKeepAlive();
+ _keepAlive.Start();
}
// Notify WebSocket about timeout
@@ -186,66 +203,26 @@ namespace Emby.Server.Implementations.Session
{
lock (_webSocketsLock)
{
- if (!_webSockets.Remove(webSocket))
- {
- _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
- }
- else
+ if (_webSockets.Remove(webSocket))
{
webSocket.Closed -= OnWebSocketClosed;
}
- }
- }
-
- ///
- /// Starts the KeepAlive watcher.
- ///
- private void StartKeepAlive()
- {
- lock (_keepAliveLock)
- {
- if (_keepAliveCancellationToken is null)
- {
- _keepAliveCancellationToken = new CancellationTokenSource();
- // Start KeepAlive watcher
- _ = RepeatAsyncCallbackEvery(
- KeepAliveSockets,
- TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
- _keepAliveCancellationToken.Token);
- }
- }
- }
-
- ///
- /// Stops the KeepAlive watcher.
- ///
- private void StopKeepAlive()
- {
- lock (_keepAliveLock)
- {
- if (_keepAliveCancellationToken is not null)
+ else
{
- _keepAliveCancellationToken.Cancel();
- _keepAliveCancellationToken.Dispose();
- _keepAliveCancellationToken = null;
+ _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
}
- }
- lock (_webSocketsLock)
- {
- foreach (var webSocket in _webSockets)
+ if (_webSockets.Count == 0)
{
- webSocket.Closed -= OnWebSocketClosed;
+ _keepAlive.Stop();
}
-
- _webSockets.Clear();
}
}
///
/// Checks status of KeepAlive of WebSockets.
///
- private async Task KeepAliveSockets()
+ private async void KeepAliveSockets(object? o, EventArgs? e)
{
List inactive;
List lost;
@@ -291,11 +268,6 @@ namespace Emby.Server.Implementations.Session
RemoveWebSocket(webSocket);
}
}
-
- if (_webSockets.Count == 0)
- {
- StopKeepAlive();
- }
}
}
@@ -310,29 +282,5 @@ namespace Emby.Server.Implementations.Session
new ForceKeepAliveMessage(WebSocketLostTimeout),
CancellationToken.None);
}
-
- ///
- /// Runs a given async callback once every specified interval time, until cancelled.
- ///
- /// The async callback.
- /// The interval time.
- /// The cancellation token.
- /// Task.
- private async Task RepeatAsyncCallbackEvery(Func callback, TimeSpan interval, CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await callback().ConfigureAwait(false);
-
- try
- {
- await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- return;
- }
- }
- }
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 34c9e86f26..c1a615666c 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
}
string? presentationUniqueKey = null;
- int? limit = null;
+ int? limit = request.Limit;
if (!request.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 965b7e7e60..e425000cd6 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,10 +1,7 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
-using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
@@ -15,61 +12,44 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
public class FirstTimeSetupHandler : AuthorizationHandler
{
private readonly IConfigurationManager _configurationManager;
- private readonly IUserManager _userManager;
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
- /// Instance of the interface.
- public FirstTimeSetupHandler(
- IConfigurationManager configurationManager,
- IUserManager userManager)
+ public FirstTimeSetupHandler(IConfigurationManager configurationManager)
{
_configurationManager = configurationManager;
- _userManager = userManager;
}
///
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{
+ // Succeed if the startup wizard / first time setup is not complete
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(requirement);
- return Task.CompletedTask;
}
- var contextUser = context.User;
- if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator))
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
- var userId = contextUser.GetUserId();
- if (userId.IsEmpty())
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
- if (!requirement.ValidateParentalSchedule)
+ // Succeed if user is admin
+ else if (context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
- return Task.CompletedTask;
}
- var user = _userManager.GetUserById(userId);
- if (user is null)
+ // Fail if admin is required and user is not admin
+ else if (requirement.RequireAdmin)
{
- throw new ResourceNotFoundException();
+ context.Fail();
}
- if (user.IsParentalScheduleAllowed())
+ // 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;
}
}
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 72be555133..8954c8ef5c 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -83,6 +83,7 @@ public class AudioController : BaseJellyfinApiController
/// Optional. The index of the video stream to use. If omitted the first video stream will be used.
/// Optional. The .
/// Optional. The streaming options.
+ /// Optional. Whether to enable Audio Encoding.
/// Audio stream returned.
/// A containing the audio file.
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
@@ -138,7 +139,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary? streamOptions)
+ [FromQuery] Dictionary? streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
@@ -189,7 +191,8 @@ public class AudioController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
@@ -247,6 +250,7 @@ public class AudioController : BaseJellyfinApiController
/// Optional. The index of the video stream to use. If omitted the first video stream will be used.
/// Optional. The .
/// Optional. The streaming options.
+ /// Optional. Whether to enable Audio Encoding.
/// Audio stream returned.
/// A containing the audio file.
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
@@ -302,7 +306,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary? streamOptions)
+ [FromQuery] Dictionary? streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
@@ -353,7 +358,8 @@ public class AudioController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 076084c7a3..ee912a9be8 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
@@ -45,9 +46,9 @@ public class DashboardController : BaseJellyfinApiController
/// Server still loading.
/// An with infos about the plugins.
[HttpGet("web/ConfigurationPages")]
+ [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize]
public ActionResult> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 1cad663264..6d94d96f3a 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -194,7 +194,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
- if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type))
+ if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 49fc2f3d78..329dd2c4cb 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -156,6 +156,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The max width.
/// Optional. The max height.
/// Optional. Whether to enable subtitles in the manifest.
+ /// Optional. Whether to enable Audio Encoding.
/// Hls live stream retrieved.
/// A containing the hls file.
[HttpGet("Videos/{itemId}/live.m3u8")]
@@ -213,7 +214,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] Dictionary streamOptions,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
- [FromQuery] bool? enableSubtitlesInManifest)
+ [FromQuery] bool? enableSubtitlesInManifest,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
VideoRequestDto streamingRequest = new VideoRequestDto
{
@@ -267,7 +269,8 @@ public class DynamicHlsController : BaseJellyfinApiController
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
- EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
+ EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
// CTS lifecycle is managed internally.
@@ -393,6 +396,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The streaming options.
/// Enable adaptive bitrate streaming.
/// Enable trickplay image playlists being added to master playlist.
+ /// Whether to enable Audio Encoding.
/// Video stream returned.
/// A containing the playlist file.
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -451,7 +455,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true,
- [FromQuery] bool enableTrickplay = true)
+ [FromQuery] bool enableTrickplay = true,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -505,7 +510,8 @@ public class DynamicHlsController : BaseJellyfinApiController
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
- EnableTrickplay = enableTrickplay
+ EnableTrickplay = enableTrickplay,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -564,6 +570,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The .
/// Optional. The streaming options.
/// Enable adaptive bitrate streaming.
+ /// Optional. Whether to enable Audio Encoding.
/// Audio stream returned.
/// A containing the playlist file.
[HttpGet("Audio/{itemId}/master.m3u8")]
@@ -620,7 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new HlsAudioRequestDto
{
@@ -671,7 +679,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -730,6 +739,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The index of the video stream to use. If omitted the first video stream will be used.
/// Optional. The .
/// Optional. The streaming options.
+ /// Optional. Whether to enable Audio Encoding.
/// Video stream returned.
/// A containing the audio file.
[HttpGet("Videos/{itemId}/main.m3u8")]
@@ -785,7 +795,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary streamOptions)
+ [FromQuery] Dictionary streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
@@ -838,7 +849,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -897,6 +909,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The index of the video stream to use. If omitted the first video stream will be used.
/// Optional. The .
/// Optional. The streaming options.
+ /// Optional. Whether to enable Audio Encoding.
/// Audio stream returned.
/// A containing the audio file.
[HttpGet("Audio/{itemId}/main.m3u8")]
@@ -951,7 +964,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary streamOptions)
+ [FromQuery] Dictionary streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new StreamingRequestDto
@@ -1002,7 +1016,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -1067,6 +1082,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The index of the video stream to use. If omitted the first video stream will be used.
/// Optional. The .
/// Optional. The streaming options.
+ /// Optional. Whether to enable Audio Encoding.
/// Video stream returned.
/// A containing the audio file.
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1128,7 +1144,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary streamOptions)
+ [FromQuery] Dictionary streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new VideoRequestDto
{
@@ -1183,7 +1200,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1247,6 +1265,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The index of the video stream to use. If omitted the first video stream will be used.
/// Optional. The .
/// Optional. The streaming options.
+ /// Optional. Whether to enable Audio Encoding.
/// Video stream returned.
/// A containing the audio file.
[HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1307,7 +1326,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary streamOptions)
+ [FromQuery] Dictionary streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new StreamingRequestDto
{
@@ -1360,7 +1380,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1481,7 +1502,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (currentTranscodingIndex.HasValue)
{
- DeleteLastFile(playlistPath, segmentExtension, 0);
+ await DeleteLastFile(playlistPath, segmentExtension, 0).ConfigureAwait(false);
}
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
@@ -1671,8 +1692,8 @@ public class DynamicHlsController : BaseJellyfinApiController
if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2));
- if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value, audioChannels ?? 2);
+ if (_encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
audioTranscodeParams += vbrParam;
}
@@ -1712,12 +1733,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var channels = state.OutputAudioChannels;
+ var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None;
+
if (channels.HasValue
&& (channels.Value != 2
- || (state.AudioStream is not null
- && state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value > 5
- && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
+ || (state.AudioStream?.Channels != null && !useDownMixAlgorithm)))
{
args += " -ac " + channels.Value;
}
@@ -1725,8 +1745,8 @@ public class DynamicHlsController : BaseJellyfinApiController
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2));
- if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value, channels ?? 2);
+ if (_encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
args += vbrParam;
}
@@ -1740,6 +1760,12 @@ public class DynamicHlsController : BaseJellyfinApiController
{
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
+ else if (state.AudioStream?.CodecTag is not null && state.AudioStream.CodecTag.Equals("ac-4", StringComparison.Ordinal))
+ {
+ // ac-4 audio tends to hava a super weird sample rate that will fail most encoders
+ // force resample it to 48KHz
+ args += " -ar 48000";
+ }
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
@@ -2010,17 +2036,19 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
- private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
- if (file is not null)
+ if (file is null)
{
- DeleteFile(file.FullName, retryCount);
+ return Task.CompletedTask;
}
+
+ return DeleteFile(file.FullName, retryCount);
}
- private void DeleteFile(string path, int retryCount)
+ private async Task DeleteFile(string path, int retryCount)
{
if (retryCount >= 5)
{
@@ -2037,9 +2065,8 @@ public class DynamicHlsController : BaseJellyfinApiController
{
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- var task = Task.Delay(100);
- task.Wait();
- DeleteFile(path, retryCount + 1);
+ await Task.Delay(100).ConfigureAwait(false);
+ await DeleteFile(path, retryCount + 1).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d6e043e6a1..4abca32713 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -162,7 +162,7 @@ public class FilterController : BaseJellyfinApiController
}
else if (parentId.HasValue)
{
- parentItem = _libraryManager.GetItemById(parentId.Value);
+ parentItem = _libraryManager.GetItemById(parentId.Value);
}
var filters = new QueryFilters();
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 6b38fa7d34..8e8accab3c 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -90,6 +90,7 @@ public class ImageController : BaseJellyfinApiController
/// User Id.
/// Image updated.
/// User does not have permission to delete the image.
+ /// Item not found.
/// A .
[HttpPost("UserImage")]
[Authorize]
@@ -97,6 +98,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task PostUserImage(
[FromQuery] Guid? userId)
{
@@ -289,7 +291,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromQuery] int? imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -317,7 +319,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -346,7 +348,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -390,7 +392,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -433,7 +435,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] int imageIndex,
[FromQuery, Required] int newIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -456,7 +458,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -559,7 +561,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromQuery] int? imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -637,7 +639,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -715,7 +717,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromRoute, Required] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 3cf4852995..dcbacf1d78 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -62,9 +62,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("Songs/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetInstantMixFromSong(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -75,11 +77,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -99,9 +106,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("Albums/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetInstantMixFromAlbum(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -112,15 +121,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var album = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -136,9 +150,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("Playlists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetInstantMixFromPlaylist(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -149,15 +165,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var playlist = (Playlist)_libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -209,9 +230,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("Artists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetInstantMixFromArtists(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -222,11 +245,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -246,9 +274,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("Items/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetInstantMixFromItem(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -259,11 +289,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -283,9 +318,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Use GetInstantMixFromArtists")]
public ActionResult> GetInstantMixFromArtists2(
[FromQuery, Required] Guid id,
@@ -320,9 +357,11 @@ public class InstantMixController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Instant playlist returned.
+ /// Item not found.
/// A with the playlist items.
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
@@ -333,11 +372,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(id, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index e3aee1bf7a..d009f80a96 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -64,7 +66,7 @@ public class ItemLookupController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetExternalIdInfos([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -234,6 +236,7 @@ public class ItemLookupController : BaseJellyfinApiController
/// The remote search result.
/// Optional. Whether or not to replace all images. Default: True.
/// Item metadata refreshed.
+ /// Item not found.
///
/// A that represents the asynchronous operation to get the remote search results.
/// The task result contains an .
@@ -241,12 +244,18 @@ public class ItemLookupController : BaseJellyfinApiController
[HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task ApplySearchCriteria(
[FromRoute, Required] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult,
[FromQuery] bool replaceAllImages = true)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
+
_logger.LogInformation(
"Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
item.Id,
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 0a8522e1cf..d7a8c37c4b 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -2,7 +2,10 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
@@ -61,7 +64,7 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -77,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|| replaceAllImages
|| replaceAllMetadata,
- IsAutomated = false
+ IsAutomated = false,
+ RemoveOldMetadata = replaceAllMetadata
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9800248c68..4001a6addb 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -5,6 +5,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
@@ -72,7 +74,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -145,7 +147,11 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var info = new MetadataEditorInfo
{
@@ -197,7 +203,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -258,7 +264,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.Studios is not null)
{
- item.Studios = request.Studios.Select(x => x.Name).ToArray();
+ item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
}
if (request.DateCreated.HasValue)
@@ -282,19 +288,37 @@ public class ItemUpdateController : BaseJellyfinApiController
if (item is Series rseries)
{
- foreach (Season season in rseries.Children)
+ foreach (var season in rseries.Children.OfType())
{
- season.OfficialRating = request.OfficialRating;
+ if (!season.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ season.OfficialRating = request.OfficialRating;
+ }
+
season.CustomRating = request.CustomRating;
- season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!season.LockedFields.Contains(MetadataField.Tags))
+ {
+ season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- foreach (Episode ep in season.Children)
+ foreach (var ep in season.Children.OfType())
{
- ep.OfficialRating = request.OfficialRating;
+ if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ ep.OfficialRating = request.OfficialRating;
+ }
+
ep.CustomRating = request.CustomRating;
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!ep.LockedFields.Contains(MetadataField.Tags))
+ {
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -302,11 +326,20 @@ public class ItemUpdateController : BaseJellyfinApiController
}
else if (item is Season season)
{
- foreach (Episode ep in season.Children)
+ foreach (var ep in season.Children.OfType())
{
- ep.OfficialRating = request.OfficialRating;
+ if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ ep.OfficialRating = request.OfficialRating;
+ }
+
ep.CustomRating = request.CustomRating;
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!ep.LockedFields.Contains(MetadataField.Tags))
+ {
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -315,9 +348,18 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (BaseItem track in album.Children)
{
- track.OfficialRating = request.OfficialRating;
+ if (!track.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ track.OfficialRating = request.OfficialRating;
+ }
+
track.CustomRating = request.CustomRating;
- track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!track.LockedFields.Contains(MetadataField.Tags))
+ {
+ track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
track.OnMetadataChanged();
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -373,10 +415,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
- hasAlbumArtists.AlbumArtists = request
- .AlbumArtists
- .Select(i => i.Name)
- .ToArray();
+ hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
}
}
@@ -384,10 +423,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
- hasArtists.Artists = request
- .ArtistItems
- .Select(i => i.Name)
- .ToArray();
+ hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 26ae1a820f..d33634412b 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -76,6 +76,7 @@ public class ItemsController : BaseJellyfinApiController
/// Optional filter by items with special features.
/// Optional filter by items with trailers.
/// Optional. Return items that are siblings of a supplied item.
+ /// Optional filter by index number.
/// Optional filter by parent index number.
/// Optional filter by items that have or do not have a parental rating.
/// Optional filter by items that are HD or not.
@@ -165,6 +166,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
[FromQuery] Guid? adjacentTo,
+ [FromQuery] int? indexNumber,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
@@ -246,9 +248,9 @@ public class ItemsController : BaseJellyfinApiController
var isApiKey = User.GetIsApiKey();
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
userId = RequestHelpers.GetUserId(User, userId);
- var user = !isApiKey && !userId.IsNullOrEmpty()
- ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
- : null;
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException();
// beyond this point, we're either using an api key or we have a valid user
if (!isApiKey && user is null)
@@ -256,6 +258,13 @@ public class ItemsController : BaseJellyfinApiController
return BadRequest("userId is required");
}
+ if (user is not null
+ && user.GetPreference(PreferenceKind.AllowedTags).Length != 0
+ && !fields.Contains(ItemFields.Tags))
+ {
+ fields = [..fields, ItemFields.Tags];
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -359,6 +368,7 @@ public class ItemsController : BaseJellyfinApiController
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
ParentId = parentId ?? Guid.Empty,
+ IndexNumber = indexNumber,
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = excludeItemIds,
@@ -710,6 +720,7 @@ public class ItemsController : BaseJellyfinApiController
hasSpecialFeature,
hasTrailer,
adjacentTo,
+ null,
parentIndexNumber,
hasParentalRating,
isHd,
@@ -967,9 +978,13 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
+ return _userDataRepository.GetUserDataDto(item, user);
}
///
@@ -1014,8 +1029,8 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
- var item = _libraryManager.GetItemById(itemId);
- if (item == null)
+ var item = _libraryManager.GetItemById(itemId, user);
+ if (item is null)
{
return NotFound();
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 984dc77896..62cb593351 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -102,7 +102,7 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public ActionResult GetFile([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -131,6 +131,8 @@ public class LibraryController : BaseJellyfinApiController
/// The item id.
/// Optional. Filter by user id, and attach user data.
/// Optional. Determines whether or not parent items should be searched for theme media.
+ /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.
+ /// Optional. Sort Order - Ascending, Descending.
/// Theme songs returned.
/// Item not found.
/// The item theme songs.
@@ -141,7 +143,9 @@ public class LibraryController : BaseJellyfinApiController
public ActionResult GetThemeSongs(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
+ [FromQuery] bool inheritFromParent = false,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -152,20 +156,23 @@ public class LibraryController : BaseJellyfinApiController
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById(itemId, user);
if (item is null)
{
- return NotFound("Item not found.");
+ return NotFound();
}
- IEnumerable themeItems;
+ sortOrder ??= [];
+ sortBy ??= [];
+ var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder);
+
+ IReadOnlyList themeItems;
while (true)
{
- themeItems = item.GetThemeSongs();
+ themeItems = item.GetThemeSongs(user, orderBy);
- if (themeItems.Any() || !inheritFromParent)
+ if (themeItems.Count > 0 || !inheritFromParent)
{
break;
}
@@ -198,6 +205,8 @@ public class LibraryController : BaseJellyfinApiController
/// The item id.
/// Optional. Filter by user id, and attach user data.
/// Optional. Determines whether or not parent items should be searched for theme media.
+ /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.
+ /// Optional. Sort Order - Ascending, Descending.
/// Theme videos returned.
/// Item not found.
/// The item theme videos.
@@ -208,29 +217,33 @@ public class LibraryController : BaseJellyfinApiController
public ActionResult GetThemeVideos(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
+ [FromQuery] bool inheritFromParent = false,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
-
var item = itemId.IsEmpty()
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById(itemId, user);
if (item is null)
{
- return NotFound("Item not found.");
+ return NotFound();
}
+ sortOrder ??= [];
+ sortBy ??= [];
+ var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder);
+
IEnumerable themeItems;
while (true)
{
- themeItems = item.GetThemeVideos();
+ themeItems = item.GetThemeVideos(user, orderBy);
if (themeItems.Any() || !inheritFromParent)
{
@@ -265,6 +278,8 @@ public class LibraryController : BaseJellyfinApiController
/// The item id.
/// Optional. Filter by user id, and attach user data.
/// Optional. Determines whether or not parent items should be searched for theme media.
+ /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.
+ /// Optional. Sort Order - Ascending, Descending.
/// Theme songs and videos returned.
/// Item not found.
/// The item theme videos.
@@ -274,19 +289,26 @@ public class LibraryController : BaseJellyfinApiController
public ActionResult GetThemeMedia(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
+ [FromQuery] bool inheritFromParent = false,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
{
var themeSongs = GetThemeSongs(
itemId,
userId,
- inheritFromParent);
+ inheritFromParent,
+ sortBy,
+ sortOrder);
var themeVideos = GetThemeVideos(
itemId,
userId,
- inheritFromParent);
+ inheritFromParent,
+ sortBy,
+ sortOrder);
- if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+ if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }
+ || themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound })
{
return NotFound();
}
@@ -327,6 +349,7 @@ public class LibraryController : BaseJellyfinApiController
/// The item id.
/// Item deleted.
/// Unauthorized access.
+ /// Item not found.
/// A .
[HttpDelete("Items/{itemId}")]
[Authorize]
@@ -335,17 +358,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItem(Guid itemId)
{
- var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
- var user = !isApiKey && !userId.IsEmpty()
- ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
- : null;
- if (!isApiKey && user is null)
+ var isApiKey = User.GetIsApiKey();
+ var user = userId.IsEmpty() && isApiKey
+ ? null
+ : _userManager.GetUserById(userId);
+
+ if (user is null && !isApiKey)
{
- return Unauthorized("Unauthorized access");
+ return NotFound();
}
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById(itemId, user);
if (item is null)
{
return NotFound();
@@ -391,7 +415,7 @@ public class LibraryController : BaseJellyfinApiController
foreach (var i in ids)
{
- var item = _libraryManager.GetItemById(i);
+ var item = _libraryManager.GetItemById(i, user);
if (item is null)
{
return NotFound();
@@ -459,20 +483,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
-
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById(itemId, user);
if (item is null)
{
- return NotFound("Item not found");
+ return NotFound();
}
var baseItemDtos = new List();
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
-
var dtoOptions = new DtoOptions().AddClientFields(User);
BaseItem? parent = item.GetParent();
@@ -520,7 +542,11 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetMediaFolders([FromQuery] bool? isHidden)
{
- var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
+ var items = _libraryManager.GetUserRootFolder().Children
+ .Concat(_libraryManager.RootFolder.VirtualChildren)
+ .Where(i => _libraryManager.GetLibraryOptions(i).Enabled)
+ .OrderBy(i => i.SortName)
+ .ToList();
if (isHidden.HasValue)
{
@@ -640,14 +666,16 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public async Task GetDownload([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var userId = User.GetUserId();
+ var user = userId.IsEmpty()
+ ? null
+ : _userManager.GetUserById(userId);
+ var item = _libraryManager.GetItemById(itemId, user);
if (item is null)
{
return NotFound();
}
- var user = _userManager.GetUserById(User.GetUserId());
-
if (user is not null)
{
if (!item.CanDownload(user))
@@ -700,12 +728,14 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
var item = itemId.IsEmpty()
- ? (userId.IsNullOrEmpty()
+ ? (user is null
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById(itemId, user);
if (item is null)
{
return NotFound();
@@ -716,9 +746,6 @@ public class LibraryController : BaseJellyfinApiController
return new QueryResult();
}
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User);
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 23c430f859..93c2393f33 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -6,6 +6,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Api;
@@ -73,7 +75,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task AddVirtualFolder(
- [FromQuery] string? name,
+ [FromQuery] string name,
[FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
@@ -83,7 +85,7 @@ public class LibraryStructureController : BaseJellyfinApiController
if (paths is not null && paths.Length > 0)
{
- libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
+ libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i));
}
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
@@ -101,7 +103,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task RemoveVirtualFolder(
- [FromQuery] string? name,
+ [FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
@@ -178,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
- await _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
+ var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
+ if (newLib is CollectionFolder folder)
+ {
+ foreach (var child in folder.GetPhysicalFolders())
+ {
+ await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+ await child.ValidateChildren(new Progress(), 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(), CancellationToken.None).ConfigureAwait(false);
+ }
}
else
{
@@ -265,18 +281,16 @@ public class LibraryStructureController : BaseJellyfinApiController
/// Whether to refresh the library.
/// A .
/// Media path removed.
- /// The name of the library may not be empty.
+ /// The name of the library and path may not be empty.
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath(
- [FromQuery] string? name,
- [FromQuery] string? path,
+ [FromQuery] string name,
+ [FromQuery] string path,
[FromQuery] bool refreshLibrary = false)
{
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
_libraryMonitor.Stop();
@@ -311,15 +325,21 @@ public class LibraryStructureController : BaseJellyfinApiController
///
/// The library name and options.
/// Library updated.
+ /// Item not found.
/// A .
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request)
{
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
+ var item = _libraryManager.GetItemById(request.Id);
+ if (item is null)
+ {
+ return NotFound();
+ }
- collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
+ item.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 7768b3c45f..2b26c01f88 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -220,9 +220,11 @@ public class LiveTvController : BaseJellyfinApiController
/// Channel id.
/// Optional. Attach user data.
/// Live tv channel returned.
+ /// Item not found.
/// An containing the live tv channel.
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
@@ -232,7 +234,12 @@ public class LiveTvController : BaseJellyfinApiController
: _userManager.GetUserById(userId.Value);
var item = channelId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(channelId);
+ : _libraryManager.GetItemById(channelId, user);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -416,9 +423,11 @@ public class LiveTvController : BaseJellyfinApiController
/// Recording id.
/// Optional. Attach user data.
/// Recording returned.
+ /// Item not found.
/// An containing the live tv recording.
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
@@ -426,7 +435,13 @@ public class LiveTvController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+ var item = recordingId.IsEmpty()
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(recordingId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -611,7 +626,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
+ var series = _libraryManager.GetItemById(librarySeriesId.Value);
+ if (series is not null)
{
query.Name = series.Name;
}
@@ -665,7 +681,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
+ var series = _libraryManager.GetItemById(body.LibrarySeriesId);
+ if (series is not null)
{
query.Name = series.Name;
}
@@ -779,7 +796,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
- var item = _libraryManager.GetItemById(recordingId);
+ var item = _libraryManager.GetItemById(recordingId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs
index f2b312b478..8eb4cadf88 100644
--- a/Jellyfin.Api/Controllers/LyricsController.cs
+++ b/Jellyfin.Api/Controllers/LyricsController.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities.Audio;
@@ -66,37 +67,16 @@ public class LyricsController : BaseJellyfinApiController
[HttpGet("Audio/{itemId}/Lyrics")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> GetLyrics([FromRoute, Required] Guid itemId)
{
- var isApiKey = User.GetIsApiKey();
- var userId = User.GetUserId();
- if (!isApiKey && userId.IsEmpty())
- {
- return BadRequest();
- }
-
- var audio = _libraryManager.GetItemById