diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index 14df7e7c8d..8d0737b66c 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -7,7 +7,7 @@ parameters: default: "ubuntu-latest" - name: DotNetSdkVersion type: string - default: 5.0.100 + default: 5.0.103 jobs: - job: CompatibilityCheck diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml deleted file mode 100644 index 177f78889c..0000000000 --- a/.ci/azure-pipelines-api-client.yml +++ /dev/null @@ -1,59 +0,0 @@ -parameters: - - name: LinuxImage - type: string - default: "ubuntu-latest" - - name: GeneratorVersion - type: string - default: "5.0.0-beta2" - -jobs: -- job: GenerateApiClients - displayName: 'Generate Api Clients' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - dependsOn: Test - - pool: - vmImage: "${{ parameters.LinuxImage }}" - - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download OpenAPI Spec Artifact' - inputs: - source: 'current' - artifact: "OpenAPI Spec" - path: "$(System.ArtifactsDirectory)/openapispec" - runVersion: "latest" - - - task: CmdLine@2 - displayName: 'Download OpenApi Generator' - inputs: - script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" - -## Authenticate with npm registry - - task: npmAuthenticate@0 - inputs: - workingFile: ./.npmrc - customEndpoint: 'jellyfin-bot for NPM' - -## Generate npm api client - - task: CmdLine@2 - displayName: 'Build stable typescript axios client' - inputs: - script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" - -## Run npm install - - task: Npm@1 - displayName: 'Install npm dependencies' - inputs: - command: install - workingDir: ./apiclient/generated/typescript/axios - -## Publish npm packages - - task: Npm@1 - displayName: 'Publish stable typescript axios client' - inputs: - command: custom - customCommand: publish --access public - publishRegistry: useExternalRegistry - publishEndpoint: 'jellyfin-bot for NPM' - workingDir: ./apiclient/generated/typescript/axios diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index 95dd3ccac2..4bc72f9eb0 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -1,7 +1,7 @@ parameters: LinuxImage: 'ubuntu-latest' RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' - DotNetSdkVersion: 5.0.100 + DotNetSdkVersion: 5.0.103 jobs: - job: Build diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 47477ba602..543fd7fc6d 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -22,6 +22,12 @@ jobs: BuildConfiguration: ubuntu.armhf Linux.amd64: BuildConfiguration: linux.amd64 + Linux.amd64-musl: + BuildConfiguration: linux.amd64-musl + Linux.arm64: + BuildConfiguration: linux.arm64 + Linux.armhf: + BuildConfiguration: linux.armhf Windows.amd64: BuildConfiguration: windows.amd64 MacOS: @@ -154,7 +160,6 @@ jobs: dependsOn: - BuildPackage - BuildDocker - condition: and(succeeded('BuildPackage'), succeeded('BuildDocker')) pool: vmImage: 'ubuntu-latest' @@ -180,13 +185,14 @@ jobs: - job: PublishNuget displayName: 'Publish NuGet packages' - dependsOn: - - BuildPackage - condition: succeeded('BuildPackage') pool: vmImage: 'ubuntu-latest' + variables: + - name: JellyfinVersion + value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')] + steps: - task: UseDotNet@2 displayName: 'Use .NET 5.0 sdk' @@ -198,12 +204,19 @@ jobs: displayName: 'Build Stable Nuget packages' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: - command: 'pack' - packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj' - versioningScheme: 'off' + command: 'custom' + projects: | + Jellyfin.Data/Jellyfin.Data.csproj + MediaBrowser.Common/MediaBrowser.Common.csproj + MediaBrowser.Controller/MediaBrowser.Controller.csproj + MediaBrowser.Model/MediaBrowser.Model.csproj + Emby.Naming/Emby.Naming.csproj + custom: 'pack' + arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion) - task: DotNetCoreCLI@2 displayName: 'Build Unstable Nuget packages' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: command: 'custom' projects: | @@ -226,7 +239,7 @@ jobs: condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: command: 'push' - packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' nuGetFeedType: 'external' publishFeedCredentials: 'NugetOrg' allowPackageConflicts: true # This ignores an error if the version already exists diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 36152c82a4..7838b3b026 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -10,7 +10,7 @@ parameters: default: "tests/**/*Tests.csproj" - name: DotNetSdkVersion type: string - default: 5.0.100 + default: 5.0.103 jobs: - job: Test @@ -94,5 +94,5 @@ jobs: displayName: 'Publish OpenAPI Artifact' condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) inputs: - targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json" + targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json" artifactName: 'OpenAPI Spec' diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index ec4c254358..c028b6e3e8 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -6,7 +6,7 @@ variables: - name: RestoreBuildProjects value: 'Jellyfin.Server/Jellyfin.Server.csproj' - name: DotNetSdkVersion - value: 5.0.100 + value: 5.0.103 pr: autoCancel: true @@ -61,6 +61,3 @@ jobs: - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - template: azure-pipelines-package.yml - -- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - - template: azure-pipelines-api-client.yml diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 87c8e414e9..0000000000 --- a/.drone.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -kind: pipeline -name: build-debug - -steps: -- name: submodules - image: docker:git - commands: - - git submodule update --init --recursive - -- name: build - image: microsoft/dotnet:2-sdk - commands: - - dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug" - ---- -kind: pipeline -name: build-release - -steps: -- name: submodules - image: docker:git - commands: - - git submodule update --init --recursive - -- name: build - image: microsoft/dotnet:2-sdk - commands: - - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release" - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d67e1c98bf..12f1f5ed53 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -33,7 +33,13 @@ assignees: '' **Expected behavior** -**Logs** +**Server Logs** + + +**FFmpeg Logs** + + +**Browser Console Logs** **Screenshots** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0874cae2e3..70bcd49737 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,4 +6,10 @@ updates: interval: weekly time: '12:00' open-pull-requests-limit: 10 - + +- package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + time: '12:00' + open-pull-requests-limit: 10 diff --git a/.github/label-commenter-config.yml b/.github/label-commenter-config.yml new file mode 100644 index 0000000000..0ff3a7f877 --- /dev/null +++ b/.github/label-commenter-config.yml @@ -0,0 +1,43 @@ +comment: + header: Hello! + footer: "\ + ---\n\n + > This is an automated comment created by the [peaceiris/actions-label-commenter]. \ + Responding to the bot or mentioning it won't have any effect.\n\n + [peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter + " + +labels: + - name: stable backport + labeled: + pr: + body: | + This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release. + + Please observe the following: + + * Any dependent PRs that this PR requires **must** be tagged for stable backporting as well. + + * Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release. + + * This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided. + + To do this, run the following commands from your local copy of the Jellyfin repository: + + 1. `git checkout master` + + 1. `git merge --no-ff ` + + 1. `git log` -> `commit xxxxxxxxx`, grab hash + + 1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`) + + 1. `git cherry-pick -sx -m1 ` + + Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff. + + Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly. + + **Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state. + + Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable. diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml new file mode 100644 index 0000000000..01998b8526 --- /dev/null +++ b/.github/workflows/automation.yml @@ -0,0 +1,64 @@ +name: Automation + +on: + pull_request_target: + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Does PR has the stable backport label? + uses: Dreamcodeio/does-pr-has-label@v1.2 + id: checkLabel + with: + label: stable backport + + - name: Remove from 'Current Release' project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel + continue-on-error: true + with: + project: Current Release + action: delete + repo-token: ${{ secrets.JF_BOT_TOKEN }} + + - name: Add to 'Release Next' project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened' + continue-on-error: true + with: + project: Release Next + column: In progress + repo-token: ${{ secrets.JF_BOT_TOKEN }} + + - name: Add to 'Current Release' project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel + continue-on-error: true + with: + project: Current Release + column: In progress + repo-token: ${{ secrets.JF_BOT_TOKEN }} + + - name: Check number of comments from the team member + if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' + id: member_comments + run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)" + + - name: Move issue to needs triage + uses: alex-page/github-project-automation-plus@v0.7.1 + if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1 + continue-on-error: true + with: + project: Issue Triage for Main Repo + column: Needs triage + repo-token: ${{ secrets.JF_BOT_TOKEN }} + + - name: Add issue to triage project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: github.event.issue.pull_request == '' && github.event.action == 'opened' + continue-on-error: true + with: + project: Issue Triage for Main Repo + column: Pending response + repo-token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/check-backport.yml b/.github/workflows/check-backport.yml new file mode 100644 index 0000000000..9ec58a3314 --- /dev/null +++ b/.github/workflows/check-backport.yml @@ -0,0 +1,96 @@ +name: Stable Backport Check +on: + issue_comment: + types: + - created + - edited + pull_request_target: + types: + - labeled + - synchronize + +jobs: + check-backport: + name: Check Backport + if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }} + runs-on: ubuntu-latest + steps: + - name: Notify as seen + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + - name: Checkout the latest code + uses: actions/checkout@v2 + with: + token: ${{ secrets.JF_BOT_TOKEN }} + fetch-depth: 0 + + - name: Notify as running + id: comment_running + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + Running backport tests... + + - name: Perform test backport + id: run_tests + run: | + set +o errexit + git config --global user.name "Jellyfin Bot" + git config --global user.email "team@jellyfin.org" + CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}" + git checkout master + git merge --no-ff ${CURRENT_BRANCH} + MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' ) + git fetch --all + CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' ) + stable_branch="Current stable release branch: ${CURRENT_STABLE}" + echo ${stable_branch} + echo ::set-output name=branch::${stable_branch} + git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE} + git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt + retcode=$? + cat output.txt | grep -v 'hint:' + output="$( grep -v 'hint:' output.txt )" + output="${output//'%'/'%25'}" + output="${output//$'\n'/'%0A'}" + output="${output//$'\r'/'%0D'}" + echo ::set-output name=output::$output + exit ${retcode} + + - name: Notify with result success + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null && success() }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ steps.comment_running.outputs.comment-id }} + body: | + ${{ steps.run_tests.outputs.branch }} + Output from `git cherry-pick`: + + --- + + ${{ steps.run_tests.outputs.output }} + reactions: hooray + + - name: Notify with result failure + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null && failure() }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ steps.comment_running.outputs.comment-id }} + body: | + ${{ steps.run_tests.outputs.branch }} + Output from `git cherry-pick`: + + --- + + ${{ steps.run_tests.outputs.output }} + reactions: confused diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5388948189..3e456f9093 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,7 +24,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '5.0.100' + dotnet-version: '5.0.x' - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: diff --git a/.github/workflows/label-commenter.yml b/.github/workflows/label-commenter.yml new file mode 100644 index 0000000000..1d4eaaecdb --- /dev/null +++ b/.github/workflows/label-commenter.yml @@ -0,0 +1,24 @@ +name: Label Commenter + +on: + issues: + types: + - labeled + - unlabeled + pull_request_target: + types: + - labeled + - unlabeled + +jobs: + comment: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + ref: master + + - name: Label Commenter + uses: peaceiris/actions-label-commenter@v1 + with: + github_token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml new file mode 100644 index 0000000000..1b04eab467 --- /dev/null +++ b/.github/workflows/merge-conflicts.yml @@ -0,0 +1,17 @@ +name: 'Merge Conflicts' + +on: + push: + branches: + - master + pull_request_target: + types: + - synchronize +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: eps1lon/actions-label-merge-conflict@v2.0.1 + with: + dirtyLabel: 'merge conflict' + repoToken: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 0000000000..8471f458e9 --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,30 @@ +name: Automatic Rebase +on: + issue_comment: + types: + - created + - edited + +jobs: + rebase: + name: Rebase + if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER' + runs-on: ubuntu-latest + steps: + - name: Notify as seen + uses: peter-evans/create-or-update-comment@v1.4.5 + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: '+1' + + - name: Checkout the latest code + uses: actions/checkout@v2 + with: + token: ${{ secrets.JF_BOT_TOKEN }} + fetch-depth: 0 + + - name: Automatic Rebase + uses: cirrus-actions/rebase@1.4 + env: + GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a63db6ed7c..b44961bf8d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -17,6 +17,7 @@ - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) - [ckcr4lyf](https://github.com/ckcr4lyf) + - [cocool97](https://github.com/cocool97) - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [crankdoofus](https://github.com/crankdoofus) - [crobibero](https://github.com/crobibero) @@ -49,6 +50,7 @@ - [h1nk](https://github.com/h1nk) - [hawken93](https://github.com/hawken93) - [HelloWorld017](https://github.com/HelloWorld017) + - [ikomhoog](https://github.com/ikomhoog) - [jftuga](https://github.com/jftuga) - [joern-h](https://github.com/joern-h) - [joshuaboniface](https://github.com/joshuaboniface) @@ -68,6 +70,7 @@ - [marius-luca-87](https://github.com/marius-luca-87) - [mark-monteiro](https://github.com/mark-monteiro) - [Matt07211](https://github.com/Matt07211) + - [Maxr1998](https://github.com/Maxr1998) - [mcarlton00](https://github.com/mcarlton00) - [mitchfizz05](https://github.com/mitchfizz05) - [MrTimscampi](https://github.com/MrTimscampi) @@ -80,6 +83,7 @@ - [nvllsvm](https://github.com/nvllsvm) - [nyanmisaka](https://github.com/nyanmisaka) - [OancaAndrei](https://github.com/OancaAndrei) + - [obradovichv](https://github.com/obradovichv) - [oddstr13](https://github.com/oddstr13) - [orryverducci](https://github.com/orryverducci) - [petermcneil](https://github.com/petermcneil) @@ -103,10 +107,11 @@ - [shemanaev](https://github.com/shemanaev) - [skaro13](https://github.com/skaro13) - [sl1288](https://github.com/sl1288) + - [Smith00101010](https://github.com/Smith00101010) - [sorinyo2004](https://github.com/sorinyo2004) - [sparky8251](https://github.com/sparky8251) - [spookbits](https://github.com/spookbits) - - [ssenart] (https://github.com/ssenart) + - [ssenart](https://github.com/ssenart) - [stanionascu](https://github.com/stanionascu) - [stevehayles](https://github.com/stevehayles) - [SuperSandro2000](https://github.com/SuperSandro2000) @@ -141,6 +146,8 @@ - [Pusta](https://github.com/pusta) - [nielsvanvelzen](https://github.com/nielsvanvelzen) - [skyfrk](https://github.com/skyfrk) + - [ianjazz246](https://github.com/ianjazz246) + - [peterspenler](https://github.com/peterspenler) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index 41dd3d081e..4e2d06b82a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ ARG DOTNET_VERSION=5.0 -FROM node:alpine as web-builder +FROM node:lts-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ - && yarn install \ + && npm ci --no-audit --unsafe-perm \ && mv dist /dist -FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 diff --git a/Dockerfile.arm b/Dockerfile.arm index e0eaca0edd..25a0de7db6 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -5,12 +5,12 @@ ARG DOTNET_VERSION=5.0 -FROM node:alpine as web-builder +FROM node:lts-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ - && yarn install \ + && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index db7de935cf..c9f19c5a39 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -5,12 +5,12 @@ ARG DOTNET_VERSION=5.0 -FROM node:alpine as web-builder +FROM node:lts-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ - && yarn install \ + && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index e63a858605..5ceeb55300 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace Emby.Dlna.Configuration diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs index fc02e17515..3ca43052a4 100644 --- a/Emby.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using Emby.Dlna.Configuration; diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs index 2f8d197a7a..1a1790ee6a 100644 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager } /// - protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter) + protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter) { if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) { diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs index 2f3107450c..7b8c504409 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 27f1fdabad..27c5b22680 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1,5 +1,6 @@ +#nullable disable + using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -7,7 +8,6 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml; -using Emby.Dlna.Configuration; using Emby.Dlna.Didl; using Emby.Dlna.Service; using Jellyfin.Data.Entities; @@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory } /// - protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter) + protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter) { if (xmlWriter == null) { @@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory /// /// Adds a "XSetBookmark" element to the xml document. /// - /// The . - private void HandleXSetBookmark(IDictionary sparams) + /// The method parameters. + private void HandleXSetBookmark(IReadOnlyDictionary sparams) { var id = sparams["ObjectID"]; @@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory return builder.ToString(); } - /// - /// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist. - /// - /// The . - /// The key. - /// The defaultValue. - /// The . - public static string GetValueOrDefault(IDictionary sparams, string key, string defaultValue) - { - if (sparams != null && sparams.TryGetValue(key, out string val)) - { - return val; - } - - return defaultValue; - } - /// /// Builds the "Browse" xml response. /// /// The . - /// The . + /// The method parameters. /// The device Id to use. - private void HandleBrowse(XmlWriter xmlWriter, IDictionary sparams, string deviceId) + private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId) { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; - var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); var provided = 0; @@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory /// Builds the response to the "X_BrowseByLetter request. /// /// The . - /// The . + /// The method parameters. /// The device id. - private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary sparams, string deviceId) + private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId) { // TODO: Implement this method HandleSearch(xmlWriter, sparams, deviceId); @@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory /// Builds a response to the "Search" request. /// /// The xmlWriter. - /// The sparams. + /// The method parameters. /// The deviceId. - private void HandleSearch(XmlWriter xmlWriter, IDictionary sparams, string deviceId) + private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId) { - var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty)); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); - var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); + var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty)); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); // sort example: dc:title, dc:date diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs index 982ae5d68e..187dc1d75a 100644 --- a/Emby.Dlna/ContentDirectory/StubType.cs +++ b/Emby.Dlna/ContentDirectory/StubType.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1602 namespace Emby.Dlna.ContentDirectory { diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index 4ea4e4e48c..8ee6325e9e 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs index d827eef26c..a7f2d4a73b 100644 --- a/Emby.Dlna/ControlResponse.cs +++ b/Emby.Dlna/ControlResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index abaf522bca..2982ce97e1 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -96,6 +98,7 @@ namespace Emby.Dlna.Didl using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) { + // If this using are changed to single lines, then write.Flush needs to be appended before the return. using (var writer = XmlWriter.Create(builder, settings)) { // writer.WriteStartDocument(); @@ -207,7 +210,8 @@ namespace Emby.Dlna.Didl var targetWidth = streamInfo.TargetWidth; var targetHeight = streamInfo.TargetHeight; - var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader( + var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader( + _profile, streamInfo.Container, streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(), @@ -598,7 +602,8 @@ namespace Emby.Dlna.Didl ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader( + var contentFeatures = ContentFeatureBuilder.BuildAudioHeader( + _profile, streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), targetAudioBitrate, @@ -973,15 +978,28 @@ namespace Emby.Dlna.Didl return; } - var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg"); + // TODO: Remove these default values + var albumArtUrlInfo = GetImageUrl( + imageInfo, + _profile.MaxAlbumArtWidth ?? 10000, + _profile.MaxAlbumArtHeight ?? 10000, + "jpg"); writer.WriteStartElement("upnp", "albumArtURI", NsUpnp); - writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); - writer.WriteString(albumartUrlInfo.url); + if (!string.IsNullOrEmpty(_profile.AlbumArtPn)) + { + writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); + } + + writer.WriteString(albumArtUrlInfo.url); writer.WriteFullEndElement(); - // TOOD: Remove these default values - var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg"); + // TODO: Remove these default values + var iconUrlInfo = GetImageUrl( + imageInfo, + _profile.MaxIconWidth ?? 48, + _profile.MaxIconHeight ?? 48, + "jpg"); writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url); if (!_profile.EnableAlbumArtInDidl) @@ -1032,8 +1050,7 @@ namespace Emby.Dlna.Didl var width = albumartUrlInfo.width ?? maxWidth; var height = albumartUrlInfo.height ?? maxHeight; - var contentFeatures = new ContentFeatureBuilder(_profile) - .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); + var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn); writer.WriteAttributeString( "protocolInfo", @@ -1205,8 +1222,7 @@ namespace Emby.Dlna.Didl if (width.HasValue && height.HasValue) { - var newSize = DrawingUtils.Resize( - new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); + var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); width = newSize.Width; height = newSize.Height; diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs index 2b86ea333f..b66f53ece2 100644 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl { public class StringWriterWithEncoding : StringWriter { - private readonly Encoding _encoding; + private readonly Encoding? _encoding; public StringWriterWithEncoding() { diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs index 4c6ca869aa..6cc6b73a0c 100644 --- a/Emby.Dlna/DlnaConfigurationFactory.cs +++ b/Emby.Dlna/DlnaConfigurationFactory.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index fedd20b68a..a1b1067040 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -7,12 +9,14 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Emby.Dlna.Profiles; using Emby.Dlna.Server; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; @@ -32,9 +36,9 @@ namespace Emby.Dlna private readonly IXmlSerializer _xmlSerializer; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; - private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly Dictionary> _profiles = new Dictionary>(StringComparer.Ordinal); @@ -43,14 +47,12 @@ namespace Emby.Dlna IFileSystem fileSystem, IApplicationPaths appPaths, ILoggerFactory loggerFactory, - IJsonSerializer jsonSerializer, IServerApplicationHost appHost) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; _appPaths = appPaths; _logger = loggerFactory.CreateLogger(); - _jsonSerializer = jsonSerializer; _appHost = appHost; } @@ -111,7 +113,7 @@ namespace Emby.Dlna if (profile != null) { - _logger.LogDebug("Found matching device profile: {0}", profile.Name); + _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name); } else { @@ -126,92 +128,57 @@ namespace Emby.Dlna var builder = new StringBuilder(); builder.AppendLine("No matching device profile found. The default will need to be used."); - builder.Append("FriendlyName:").AppendLine(profile.FriendlyName); - builder.Append("Manufacturer:").AppendLine(profile.Manufacturer); - builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl); - builder.Append("ModelDescription:").AppendLine(profile.ModelDescription); - builder.Append("ModelName:").AppendLine(profile.ModelName); - builder.Append("ModelNumber:").AppendLine(profile.ModelNumber); - builder.Append("ModelUrl:").AppendLine(profile.ModelUrl); - builder.Append("SerialNumber:").AppendLine(profile.SerialNumber); + builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName); + builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer); + builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl); + builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription); + builder.Append("ModelName: ").AppendLine(profile.ModelName); + builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber); + builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl); + builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber); _logger.LogInformation(builder.ToString()); } - private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) + /// + /// Attempts to match a device with a profile. + /// Rules: + /// - If the profile field has no value, the field matches irregardless of its contents. + /// - the profile field can be an exact match, or a reg exp. + /// + /// The of the device. + /// The of the profile. + /// True if they match. + public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) { - if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) - { - if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.Manufacturer)) - { - if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl)) - { - if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ModelDescription)) - { - if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ModelName)) - { - if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ModelNumber)) - { - if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)) - { - return false; - } - } + return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName) + && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer) + && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl) + && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription) + && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName) + && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber) + && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl) + && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber); + } - if (!string.IsNullOrEmpty(profileInfo.ModelUrl)) + private bool IsRegexOrSubstringMatch(string input, string pattern) + { + if (string.IsNullOrEmpty(pattern)) { - if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)) - { - return false; - } + // In profile identification: An empty pattern matches anything. + return true; } - if (!string.IsNullOrEmpty(profileInfo.SerialNumber)) + if (string.IsNullOrEmpty(input)) { - if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber)) - { - return false; - } + // The profile contains a value, and the device doesn't. + return false; } - return true; - } - - private bool IsRegexOrSubstringMatch(string input, string pattern) - { try { - return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return input.Equals(pattern, StringComparison.OrdinalIgnoreCase) + || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } catch (ArgumentException ex) { @@ -333,7 +300,12 @@ namespace Emby.Dlna throw new ArgumentNullException(nameof(id)); } - var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); + var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); + + if (info == null) + { + return null; + } return ParseProfileFile(info.Path, info.Info.Type); } @@ -395,7 +367,8 @@ namespace Emby.Dlna { Directory.CreateDirectory(systemProfilesPath); - using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } @@ -495,9 +468,9 @@ namespace Emby.Dlna return profile; } - var json = _jsonSerializer.SerializeToString(profile); + var json = JsonSerializer.Serialize(profile, _jsonOptions); - return _jsonSerializer.DeserializeFromString(json); + return JsonSerializer.Deserialize(json, _jsonOptions); } public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) @@ -553,7 +526,7 @@ namespace Emby.Dlna private void DumpProfiles() { - DeviceProfile[] list = new [] + DeviceProfile[] list = new[] { new SamsungSmartTvProfile(), new XboxOneProfile(), diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index bd30cc1e11..a40578e403 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -21,11 +21,11 @@ false true true + enable - @@ -78,9 +78,7 @@ - - - + diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs index 1b1bd426c5..8c82dcbf68 100644 --- a/Emby.Dlna/EventSubscriptionResponse.cs +++ b/Emby.Dlna/EventSubscriptionResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index ff81e83b5e..2e672b886b 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs index 40d73ee0e5..4fd7f81695 100644 --- a/Emby.Dlna/Eventing/EventSubscription.cs +++ b/Emby.Dlna/Eventing/EventSubscription.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index fb4454a343..0309926abb 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -5,10 +7,10 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Sockets; -using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -52,6 +54,8 @@ namespace Emby.Dlna.Main private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; private readonly object _syncLock = new object(); + private readonly NetworkConfiguration _netConfig; + private readonly bool _disabled; private PlayToManager _manager; private SsdpDevicePublisher _publisher; @@ -122,10 +126,23 @@ namespace Emby.Dlna.Main httpClientFactory, config); Current = this; + + _netConfig = config.GetConfiguration("network"); + _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps; + + if (_disabled && _config.GetDlnaConfiguration().EnableServer) + { + _logger.LogError("The DLNA specification does not support HTTPS."); + } } public static DlnaEntryPoint Current { get; private set; } + /// + /// Gets a value indicating whether the dlna server is enabled. + /// + public static bool Enabled { get; private set; } + public IContentDirectory ContentDirectory { get; private set; } public IConnectionManager ConnectionManager { get; private set; } @@ -136,6 +153,12 @@ namespace Emby.Dlna.Main { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); + if (_disabled) + { + // No use starting as dlna won't work, as we're running purely on HTTPS. + return; + } + ReloadComponents(); _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; @@ -152,6 +175,7 @@ namespace Emby.Dlna.Main private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); + Enabled = options.EnableServer; StartSsdpHandler(); @@ -206,7 +230,10 @@ namespace Emby.Dlna.Main { try { - ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); + if (communicationsServer != null) + { + ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); + } } catch (Exception ex) { @@ -290,12 +317,18 @@ namespace Emby.Dlna.Main _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); - var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); + var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); + if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl)) + { + // DLNA will only work over http, so we must reset to http:// : {port}. + uri.Scheme = "http"; + uri.Port = _netConfig.HttpServerPortNumber; + } var device = new SsdpRootDevice { CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. - Location = uri, // Must point to the URL that serves your devices UPnP description document. + Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. Address = address.Address, PrefixLength = address.PrefixLength, FriendlyName = "Jellyfin", diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index 464f71a6f1..d8fb127420 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar } /// - protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter) + protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter) { if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) { diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index 37840cd096..f3789a791c 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; -using MediaBrowser.Model.Dlna; namespace Emby.Dlna.MediaReceiverRegistrar { diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 938ce5fbf0..6c580d15bd 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); if (command == null) { return false; @@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("Setting mute"); var value = mute ? 1 : 0; - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType, value), + cancellationToken: cancellationToken) .ConfigureAwait(false); IsMuted = mute; @@ -253,7 +261,7 @@ namespace Emby.Dlna.PlayTo { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); if (command == null) { return; @@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo // Remote control will perform better Volume = value; - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType, value), + cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -278,7 +292,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); if (command == null) { return; @@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), + cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); @@ -305,7 +325,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); if (command == null) { return; @@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + post, + header: header, + cancellationToken: cancellationToken) .ConfigureAwait(false); - await Task.Delay(50).ConfigureAwait(false); + await Task.Delay(50, cancellationToken).ConfigureAwait(false); try { - await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false); + await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); } catch { @@ -343,6 +370,42 @@ namespace Emby.Dlna.PlayTo RestartTimer(true); } + /* + * SetNextAvTransport is used to specify to the DLNA device what is the next track to play. + * Without that information, the next track command on the device does not work. + */ + public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default) + { + var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); + + url = url.Replace("&", "&", StringComparison.Ordinal); + + _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); + + var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); + if (command == null) + { + return; + } + + var dictionary = new Dictionary + { + { "NextURI", url }, + { "NextURIMetaData", CreateDidlMeta(metaData) } + }; + + var service = GetAvTransportService(); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken) + .ConfigureAwait(false); + } + private static string CreateDidlMeta(string value) { if (string.IsNullOrEmpty(value)) @@ -378,6 +441,10 @@ namespace Emby.Dlna.PlayTo public async Task SetPlay(CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); + if (avCommands == null) + { + return; + } await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); @@ -388,7 +455,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); if (command == null) { return; @@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, 1), + cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); @@ -406,7 +479,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); if (command == null) { return; @@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) + await new SsdpHttpClient(_httpClientFactory) + .SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, 1), + cancellationToken: cancellationToken) .ConfigureAwait(false); TransportState = TransportState.Paused; @@ -528,7 +607,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); if (command == null) { return; @@ -578,7 +657,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); if (command == null) { return; @@ -665,6 +744,10 @@ namespace Emby.Dlna.PlayTo } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); + if (rendererCommands == null) + { + return null; + } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, @@ -733,6 +816,11 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); + if (rendererCommands == null) + { + return (false, null); + } + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, @@ -914,6 +1002,10 @@ namespace Emby.Dlna.PlayTo var httpClient = new SsdpHttpClient(_httpClientFactory); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } AvCommands = TransportCommands.Create(document); return AvCommands; @@ -942,6 +1034,10 @@ namespace Emby.Dlna.PlayTo var httpClient = new SsdpHttpClient(_httpClientFactory); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } RendererCommands = TransportCommands.Create(document); return RendererCommands; @@ -973,6 +1069,10 @@ namespace Emby.Dlna.PlayTo var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } var friendlyNames = new List(); @@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo var deviceProperties = new DeviceInfo() { - Name = string.Join(" ", friendlyNames), + Name = string.Join(' ', friendlyNames), BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) }; diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs index d3daab9e0a..2acfff4eb6 100644 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ b/Emby.Dlna/PlayTo/DeviceInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs index dabd079afd..2bc4d8cc24 100644 --- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs +++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 4861093044..0e49fd2c02 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; } + /* + * Send a message to the DLNA device to notify what is the next track in the playlist. + */ + private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken) + { + if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1) + { + // The current playing item is indeed in the play list and we are not yet at the end of the playlist. + var nextItemIndex = currentPlayListItemIndex + 1; + var nextItem = _playlist[nextItemIndex]; + + // Send the SetNextAvTransport message. + await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false); + } + } + private void OnDeviceUnavailable() { try @@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) { - if (_disposed) + if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) { return; } @@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo var newItemProgress = GetProgressInfo(streamInfo); await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); + + // Send a message to the DLNA device to notify what is the next track in the playlist. + var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId); + if (currentItemIndex >= 0) + { + _currentPlaylistIndex = currentItemIndex; + } + + await SendNextTrackMessage(currentItemIndex, CancellationToken.None); } catch (Exception ex) { @@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + return; } @@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo if (streamInfo.MediaType == DlnaProfileType.Audio) { - return new ContentFeatureBuilder(profile) - .BuildAudioHeader( + return ContentFeatureBuilder.BuildAudioHeader( + profile, streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioBitrate, @@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo if (streamInfo.MediaType == DlnaProfileType.Video) { - var list = new ContentFeatureBuilder(profile) - .BuildVideoHeader( + var list = ContentFeatureBuilder.BuildVideoHeader( + profile, streamInfo.Container, streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(), @@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + await SendNextTrackMessage(index, cancellationToken); + var streamInfo = currentitem.StreamInfo; if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) { @@ -736,6 +771,10 @@ namespace Emby.Dlna.PlayTo await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + if (EnableClientSideSeek(newItem.StreamInfo)) { await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); @@ -761,6 +800,10 @@ namespace Emby.Dlna.PlayTo await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0) { await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); @@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo var currentWait = 0; while (_device.TransportState != TransportState.Playing && currentWait < MaxWait) { - await Task.Delay(Interval).ConfigureAwait(false); + await Task.Delay(Interval, cancellationToken).ConfigureAwait(false); currentWait += Interval; } @@ -826,7 +869,7 @@ namespace Emby.Dlna.PlayTo return SendPlayCommand(data as PlayRequest, cancellationToken); } - if (name == SessionMessageType.PlayState) + if (name == SessionMessageType.Playstate) { return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); } @@ -896,16 +939,16 @@ namespace Emby.Dlna.PlayTo var parts = url.Split('/'); - for (var i = 0; i < parts.Length; i++) + for (var i = 0; i < parts.Length - 1; i++) { var part = parts[i]; if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) { - if (parts.Length > i + 1) + if (Guid.TryParse(parts[i + 1], out var result)) { - return Guid.Parse(parts[i + 1]); + return result; } } } @@ -943,11 +986,7 @@ namespace Emby.Dlna.PlayTo request.DeviceId = values.GetValueOrDefault("DeviceId"); request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); - - // Be careful, IsDirectStream==true by default (Static != false or not in query). - // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true. - request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); - + request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index a6793a7081..35bf5927c4 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo if (controller == null) { var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); + if (device == null) + { + _logger.LogError("Ignoring device as xml response is invalid."); + return; + } string deviceName = device.Properties.Name; _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); + string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress); controller = new PlayToController( sessionInfo, diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs index d14617c8a0..c7d2b28df8 100644 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs index 3f8d552636..f8a14f411f 100644 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs index deeb47918d..6661f92ac7 100644 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs index 85846166cf..5056e69ae7 100644 --- a/Emby.Dlna/PlayTo/PlaylistItem.cs +++ b/Emby.Dlna/PlayTo/PlaylistItem.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.Dlna; diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index e28840a896..6574913032 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 557bc69a73..f14f73bb6f 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -1,8 +1,9 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Globalization; -using System.IO; using System.Net.Http; using System.Net.Mime; using System.Text; @@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var reader = new StreamReader(stream, Encoding.UTF8); - return XDocument.Parse( - await reader.ReadToEndAsync().ConfigureAwait(false), - LoadOptions.PreserveWhitespace); + return await XDocument.LoadAsync( + stream, + LoadOptions.PreserveWhitespace, + cancellationToken).ConfigureAwait(false); } private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) @@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var reader = new StreamReader(stream, Encoding.UTF8); - return XDocument.Parse( - await reader.ReadToEndAsync().ConfigureAwait(false), - LoadOptions.PreserveWhitespace); + try + { + return await XDocument.LoadAsync( + stream, + LoadOptions.PreserveWhitespace, + cancellationToken).ConfigureAwait(false); + } + catch + { + return null; + } } private async Task PostSoapDataAsync( diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index 0865968ad1..b58669355d 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo public class TransportCommands { private const string CommandBase = "\r\n" + "" + "" + "" + "{2}" + "" + ""; - private List _stateVariables = new List(); - private List _serviceActions = new List(); - public List StateVariables => _stateVariables; + public List StateVariables { get; } = new List(); - public List ServiceActions => _serviceActions; + public List ServiceActions { get; } = new List(); public static TransportCommands Create(XDocument document) { @@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo { var serviceAction = new ServiceAction { - Name = container.GetValue(UPnpNamespaces.Svc + "name"), + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, }; var argumentList = serviceAction.ArgumentList; @@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo return new Argument { - Name = container.GetValue(UPnpNamespaces.Svc + "name"), - Direction = container.GetValue(UPnpNamespaces.Svc + "direction"), - RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty, + RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty }; } @@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo return new StateVariable { - Name = container.GetValue(UPnpNamespaces.Svc + "name"), - DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty, AllowedValues = allowedValues }; } @@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); } - private string BuildArgumentXml(Argument argument, string value, string commandParameter = "") + private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "") { var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs index 7068a5d24d..2058e9dc79 100644 --- a/Emby.Dlna/PlayTo/TransportState.cs +++ b/Emby.Dlna/PlayTo/TransportState.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1602 namespace Emby.Dlna.PlayTo { diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index 0d9478e42e..02d2da58d6 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index d4af72b626..8eaf12ba9a 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -1,5 +1,7 @@ #pragma warning disable CS1591 +using System; +using System.Globalization; using System.Linq; using MediaBrowser.Model.Dlna; @@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles { public DefaultProfile() { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); Name = "Generic Device"; ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; diff --git a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs index 8ab4acd1be..9f0d82b8f8 100644 --- a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs @@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*", + FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*", Manufacturer = "Sony", Headers = new[] @@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*", + Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*", Match = HeaderMatchType.Regex } } diff --git a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs index 42d253394c..dfb91817ac 100644 --- a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs @@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*", + FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*", Manufacturer = "Sony", Headers = new[] @@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*", + Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*", Match = HeaderMatchType.Regex } } diff --git a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs index 0598e83422..d59ee38d74 100644 --- a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs @@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*", + FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*", Manufacturer = "Sony", Headers = new[] @@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*", + Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*", Match = HeaderMatchType.Regex } } diff --git a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs index 3d90a1e72d..73b0fd67eb 100644 --- a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs @@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*", + FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*", Manufacturer = "Sony", Headers = new[] @@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*", + Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*", Match = HeaderMatchType.Regex } } diff --git a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs index 9188f73ef1..db8ee5750f 100644 --- a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs @@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles Identification = new DeviceIdentification { - FriendlyName = @"(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*", + FriendlyName = @"(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*", Manufacturer = "Sony", Headers = new[] @@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles new HttpHeaderInfo { Name = "X-AV-Client-Info", - Value = @".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*", + Value = @".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*", Match = HeaderMatchType.Regex } } diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs index d56b1df507..e4a7a3a596 100644 --- a/Emby.Dlna/Profiles/SonyPs3Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs @@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles Container = "ts,mpegts", Type = DlnaProfileType.Video, VideoCodec = "mpeg1video,mpeg2video,h264", - AudioCodec = "ac3,mp2,mp3,aac" + AudioCodec = "aac,ac3,mp2" }, new DirectPlayProfile { @@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles { Container = "ts", VideoCodec = "h264", - AudioCodec = "ac3,aac,mp3", + AudioCodec = "aac,ac3,mp2", Type = DlnaProfileType.Video }, new TranscodingProfile diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs index db56094e2b..985df0c9a5 100644 --- a/Emby.Dlna/Profiles/SonyPs4Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs @@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles Container = "ts,mpegts", Type = DlnaProfileType.Video, VideoCodec = "mpeg1video,mpeg2video,h264", - AudioCodec = "ac3,mp2,mp3,aac" + AudioCodec = "aac,ac3,mp2" }, new DirectPlayProfile { @@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles { Container = "ts", VideoCodec = "h264", - AudioCodec = "mp3", + AudioCodec = "aac,ac3,mp2", Type = DlnaProfileType.Video }, new TranscodingProfile diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml index f20e9fcb6f..1461db3117 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> Sony Bravia (2010) - KDL-\d{2}[EHLNPB]X\d[01]\d.* + KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].* Sony - + Microsoft Corporation diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml index e516ff512d..7c5f2b1817 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> Sony Bravia (2011) - KDL-\d{2}([A-Z]X\d2\d|CX400).* + KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).* Sony - + Microsoft Corporation diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml index 88bd1c2f53..842a8fba33 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> Sony Bravia (2012) - KDL-\d{2}[A-Z]X\d5(\d|G).* + KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).* Sony - + Microsoft Corporation diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml index 3ca9893cdc..f1135c3fe3 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> Sony Bravia (2013) - KDL-\d{2}[WR][5689]\d{2}A.* + KDL-[0-9]{2}[WR][5689][0-9]{2}A.* Sony - + Microsoft Corporation diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml index 8804a75dfa..85c7868c66 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml @@ -3,10 +3,10 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> Sony Bravia (2014) - (KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).* + (KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).* Sony - + Microsoft Corporation diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml index bafa44b828..129b188e2a 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -38,7 +38,7 @@ - + @@ -46,7 +46,7 @@ - + diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml index eb8e645b31..592119305e 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml @@ -38,7 +38,7 @@ - + @@ -46,7 +46,7 @@ - + diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 09525aae4e..3f3dfccd3a 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -250,7 +250,8 @@ namespace Emby.Dlna.Server url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); - return SecurityElement.Escape(url); + // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released + return SecurityElement.Escape(url) ?? string.Empty; } private IEnumerable GetIcons() diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 198852ec17..904c23d997 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -47,9 +47,9 @@ namespace Emby.Dlna.Service private async Task ProcessControlRequestInternalAsync(ControlRequest request) { - ControlRequestInfo requestInfo = null; + ControlRequestInfo? requestInfo = null; - using (var streamReader = new StreamReader(request.InputXml)) + using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) { var readerSettings = new XmlReaderSettings() { @@ -151,7 +151,7 @@ namespace Emby.Dlna.Service private async Task ParseBodyTagAsync(XmlReader reader) { - string namespaceURI = null, localName = null; + string? namespaceURI = null, localName = null; await reader.MoveToContentAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false); @@ -210,7 +210,7 @@ namespace Emby.Dlna.Service } } - protected abstract void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter); + protected abstract void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter); private void LogRequest(ControlRequest request) { diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 8c7d961f3e..391dda1479 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp { lock (_syncLock) { - if (_listenerCount > 0 && _deviceLocator == null) + if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null) { _deviceLocator = new SsdpDeviceLocator(_commsServer); @@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp { Location = e.DiscoveredDevice.DescriptionLocation, Headers = headers, - LocalIpAddress = e.LocalIpAddress + RemoteIpAddress = e.RemoteIpAddress }); DeviceDiscoveredInternal?.Invoke(this, args); diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs index e7a52f168f..d00eb02b46 100644 --- a/Emby.Dlna/Ssdp/SsdpExtensions.cs +++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs @@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp { public static class SsdpExtensions { - public static string GetValue(this XElement container, XName name) + public static string? GetValue(this XElement container, XName name) { var node = container.Element(name); return node?.Value; } - public static string GetAttributeValue(this XElement container, XName name) + public static string? GetAttributeValue(this XElement container, XName name) { var node = container.Attribute(name); return node?.Value; } - public static string GetDescendantValue(this XElement container, XName name) + public static string? GetDescendantValue(this XElement container, XName name) => container.Descendants(name).FirstOrDefault()?.Value; } } diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 7d479a5c65..5c5afe1c6e 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -25,7 +25,6 @@ - diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 8a2301d2d6..7d952aa23b 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Extensions; @@ -171,21 +172,31 @@ namespace Emby.Drawing return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null); int quality = options.Quality; ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); - string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); + string cacheFilePath = GetCacheFilePath( + originalImagePath, + options.Width, + options.Height, + options.MaxWidth, + options.MaxHeight, + options.FillWidth, + options.FillHeight, + quality, + dateModified, + outputFormat, + options.AddPlayedIndicator, + options.PercentPlayed, + options.UnplayedCount, + options.Blur, + options.BackgroundColor, + options.ForegroundLayer); try { if (!File.Exists(cacheFilePath)) { - if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath)) - { - options.CropWhiteSpace = false; - } - string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) @@ -246,48 +257,111 @@ namespace Emby.Drawing /// /// Gets the cache file path based on a set of parameters. /// - private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) + private string GetCacheFilePath( + string originalPath, + int? width, + int? height, + int? maxWidth, + int? maxHeight, + int? fillWidth, + int? fillHeight, + int quality, + DateTime dateModified, + ImageFormat format, + bool addPlayedIndicator, + double percentPlayed, + int? unwatchedCount, + int? blur, + string backgroundColor, + string foregroundLayer) { - var filename = originalPath - + "width=" + outputSize.Width - + "height=" + outputSize.Height - + "quality=" + quality - + "datemodified=" + dateModified.Ticks - + "f=" + format; + var filename = new StringBuilder(256); + filename.Append(originalPath); + + filename.Append(",quality="); + filename.Append(quality); + + filename.Append(",datemodified="); + filename.Append(dateModified.Ticks); + + filename.Append(",f="); + filename.Append(format); + + if (width.HasValue) + { + filename.Append(",width="); + filename.Append(width.Value); + } + + if (height.HasValue) + { + filename.Append(",height="); + filename.Append(height.Value); + } + + if (maxWidth.HasValue) + { + filename.Append(",maxwidth="); + filename.Append(maxWidth.Value); + } + + if (maxHeight.HasValue) + { + filename.Append(",maxheight="); + filename.Append(maxHeight.Value); + } + + if (fillWidth.HasValue) + { + filename.Append(",fillwidth="); + filename.Append(fillWidth.Value); + } + + if (fillHeight.HasValue) + { + filename.Append(",fillheight="); + filename.Append(fillHeight.Value); + } if (addPlayedIndicator) { - filename += "pl=true"; + filename.Append(",pl=true"); } if (percentPlayed > 0) { - filename += "p=" + percentPlayed; + filename.Append(",p="); + filename.Append(percentPlayed); } if (unwatchedCount.HasValue) { - filename += "p=" + unwatchedCount.Value; + filename.Append(",p="); + filename.Append(unwatchedCount.Value); } if (blur.HasValue) { - filename += "blur=" + blur.Value; + filename.Append(",blur="); + filename.Append(blur.Value); } if (!string.IsNullOrEmpty(backgroundColor)) { - filename += "b=" + backgroundColor; + filename.Append(",b="); + filename.Append(backgroundColor); } if (!string.IsNullOrEmpty(foregroundLayer)) { - filename += "fl=" + foregroundLayer; + filename.Append(",fl="); + filename.Append(foregroundLayer); } - filename += "v=" + Version; + filename.Append(",v="); + filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); } /// @@ -352,8 +426,13 @@ namespace Emby.Drawing } /// - public string GetImageCacheTag(User user) + public string? GetImageCacheTag(User user) { + if (user.ProfileImage == null) + { + return null; + } + return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() .ToString("N", CultureInfo.InvariantCulture); } diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 2a1cfd3da5..1c05aa9161 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -32,7 +32,7 @@ namespace Emby.Drawing => throw new NotImplementedException(); /// - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) { throw new NotImplementedException(); } diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs index 8b47dd12e4..af4aa0059f 100644 --- a/Emby.Naming/Audio/AudioFileParser.cs +++ b/Emby.Naming/Audio/AudioFileParser.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Linq; using Emby.Naming.Common; +using MediaBrowser.Common.Extensions; namespace Emby.Naming.Audio { @@ -18,8 +18,8 @@ namespace Emby.Naming.Audio /// True if file at path is audio file. public static bool IsAudioFile(string path, NamingOptions options) { - var extension = Path.GetExtension(path); - return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + var extension = Path.GetExtension(path.AsSpan()); + return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs index adf403ab6d..15702ff2ca 100644 --- a/Emby.Naming/AudioBook/AudioBookInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookInfo.cs @@ -15,13 +15,13 @@ namespace Emby.Naming.AudioBook /// List of files composing the actual audiobook. /// List of extra files. /// Alternative version of files. - public AudioBookInfo(string name, int? year, List? files, List? extras, List? alternateVersions) + public AudioBookInfo(string name, int? year, List files, List extras, List alternateVersions) { Name = name; Year = year; - Files = files ?? new List(); - Extras = extras ?? new List(); - AlternateVersions = alternateVersions ?? new List(); + Files = files; + Extras = extras; + AlternateVersions = alternateVersions; } /// diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index e9ea9b7a5d..ca53228903 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null); var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber }); - var nameWithReplacedDots = nameParserResult.Name.Replace(" ", "."); + var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.'); foreach (var group in groupedBy) { diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 035d1b2280..22a3e8bb49 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -282,7 +282,13 @@ namespace Emby.Naming.Common SupportsAbsoluteEpisodeNumbers = true }, - // Case Closed (1996-2007)/Case Closed - 317.mkv + // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names + // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name + new EpisodeExpression(@".*?(\[.*?\])+.*?(?[\w\s]+?)[\s_]*-[\s_]*(?[0-9]+).*$") + { + IsNamed = true + }, + // /server/anything_102.mp4 // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv // /server/anything_1996.11.14.mp4 @@ -299,11 +305,6 @@ namespace Emby.Naming.Common // *** End Kodi Standard Naming - // [bar] Foo - 1 [baz] - new EpisodeExpression(@".*?(\[.*?\])+.*?(?[\w\s]+?)[-\s_]+(?[0-9]+).*$") - { - IsNamed = true - }, new EpisodeExpression(@".*(\\|\/)[sS]?(?[0-9]+)[xX](?[0-9]+)[^\\\/]*$") { IsNamed = true @@ -587,7 +588,7 @@ namespace Emby.Naming.Common AudioBookNamesExpressions = new[] { // Detect year usually in brackets after name Batman (2020) - @"^(?.+?)\s*\(\s*(?\d{4})\s*\)\s*$", + @"^(?.+?)\s*\(\s*(?[0-9]{4})\s*\)\s*$", @"^\s*(?[^ ].*?)\s*$" }; diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 24c15759d4..3224ff4129 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -23,17 +23,18 @@ - + - + + Jellyfin Contributors Jellyfin.Naming - 10.7.0 + 10.8.0 https://github.com/jellyfin/jellyfin GPL-3.0-only @@ -44,7 +45,6 @@ - diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index f7df587864..c63aec64e4 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -68,6 +68,11 @@ namespace Emby.Naming.TV var parsingResult = new EpisodePathParser(_options) .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); + if (!parsingResult.Success && !isStub) + { + return null; + } + return new EpisodeInfo(path) { Container = container, diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index d11c7c99e8..6236f86c43 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -60,7 +60,7 @@ namespace Emby.Naming.TV bool supportSpecialAliases, bool supportNumericSeasonFolders) { - var filename = Path.GetFileName(path) ?? string.Empty; + string filename = Path.GetFileName(path); if (supportSpecialAliases) { diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs index 09a0cd1893..4eef3ebc5e 100644 --- a/Emby.Naming/Video/CleanStringParser.cs +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace Emby.Naming.Video @@ -16,8 +17,14 @@ namespace Emby.Naming.Video /// List of regex to parse name and year from. /// Parsing result string. /// True if parsing was successful. - public static bool TryClean(string name, IReadOnlyList expressions, out ReadOnlySpan newName) + public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList expressions, out ReadOnlySpan newName) { + if (string.IsNullOrEmpty(name)) + { + newName = ReadOnlySpan.Empty; + return false; + } + var len = expressions.Count; for (int i = 0; i < len; i++) { @@ -41,7 +48,7 @@ namespace Emby.Naming.Video return true; } - newName = string.Empty; + newName = ReadOnlySpan.Empty; return false; } } diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 1d3b36a1ad..f9d06c09be 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -29,70 +29,73 @@ namespace Emby.Naming.Video /// Path to file. /// Returns object. public ExtraResult GetExtraInfo(string path) - { - return _options.VideoExtraRules - .Select(i => GetExtraInfo(path, i)) - .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult(); - } - - private ExtraResult GetExtraInfo(string path, ExtraRule rule) { var result = new ExtraResult(); - if (rule.MediaType == MediaType.Audio) + for (var i = 0; i < _options.VideoExtraRules.Length; i++) { - if (!AudioFileParser.IsAudioFile(path, _options)) + var rule = _options.VideoExtraRules[i]; + if (rule.MediaType == MediaType.Audio) { - return result; + if (!AudioFileParser.IsAudioFile(path, _options)) + { + continue; + } } - } - else if (rule.MediaType == MediaType.Video) - { - if (!new VideoResolver(_options).IsVideoFile(path)) + else if (rule.MediaType == MediaType.Video) { - return result; + if (!new VideoResolver(_options).IsVideoFile(path)) + { + continue; + } } - } - - if (rule.RuleType == ExtraRuleType.Filename) - { - var filename = Path.GetFileNameWithoutExtension(path); - if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase)) + var pathSpan = path.AsSpan(); + if (rule.RuleType == ExtraRuleType.Filename) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; - } - } - else if (rule.RuleType == ExtraRuleType.Suffix) - { - var filename = Path.GetFileNameWithoutExtension(path); + var filename = Path.GetFileNameWithoutExtension(pathSpan); - if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0) + if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + else if (rule.RuleType == ExtraRuleType.Suffix) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; + var filename = Path.GetFileNameWithoutExtension(pathSpan); + + if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } } - } - else if (rule.RuleType == ExtraRuleType.Regex) - { - var filename = Path.GetFileName(path); + else if (rule.RuleType == ExtraRuleType.Regex) + { + var filename = Path.GetFileName(path); - var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); + var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); - if (regex.IsMatch(filename)) + if (regex.IsMatch(filename)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + else if (rule.RuleType == ExtraRuleType.DirectoryName) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; + var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan)); + if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } } - } - else if (rule.RuleType == ExtraRuleType.DirectoryName) - { - var directoryName = Path.GetFileName(Path.GetDirectoryName(path)); - if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase)) + + if (result.ExtraType != null) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; + return result; } } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index fd16774739..7b6a1705ba 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -185,8 +185,8 @@ namespace Emby.Naming.Video if (!string.IsNullOrEmpty(folderName) && folderName.Length > 1 && videos.All(i => i.Files.Count == 1 - && IsEligibleForMultiVersion(folderName, i.Files[0].Path)) - && HaveSameYear(videos)) + && IsEligibleForMultiVersion(folderName, i.Files[0].Path)) + && HaveSameYear(videos)) { var ordered = videos.OrderBy(i => i.Name).ToList(); @@ -216,26 +216,26 @@ namespace Emby.Naming.Video return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; } - private bool IsEligibleForMultiVersion(string folderName, string? testFilename) + private bool IsEligibleForMultiVersion(string folderName, string testFilePath) { - testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty; - + string testFilename = Path.GetFileNameWithoutExtension(testFilePath); if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { - if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) + // Remove the folder name before cleaning as we don't care about cleaning that part + if (folderName.Length <= testFilename.Length) { - testFilename = cleanName.ToString(); + testFilename = testFilename.Substring(folderName.Length).Trim(); } - if (folderName.Length <= testFilename.Length) + if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) { - testFilename = testFilename.Substring(folderName.Length).Trim(); + testFilename = cleanName.Trim().ToString(); } + // The CleanStringParser should have removed common keywords etc. return string.IsNullOrEmpty(testFilename) - || testFilename[0].Equals('-') - || testFilename[0].Equals('_') - || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); + || testFilename[0] == '-' + || Regex.IsMatch(testFilename, @"^\[([^]]*)\]"); } return false; diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index d7165d8d7f..27e73208c6 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -1,7 +1,8 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using Emby.Naming.Common; +using MediaBrowser.Common.Extensions; namespace Emby.Naming.Video { @@ -58,15 +59,15 @@ namespace Emby.Naming.Video } bool isStub = false; - string? container = null; + ReadOnlySpan container = ReadOnlySpan.Empty; string? stubType = null; if (!isDirectory) { - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); // Check supported extensions - if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's not supported. Check stub extensions if (!StubResolver.TryResolveFile(path, _options, out stubType)) @@ -85,9 +86,7 @@ namespace Emby.Naming.Video var extraResult = new ExtraResolver(_options).GetExtraInfo(path); - var name = isDirectory - ? Path.GetFileName(path) - : Path.GetFileNameWithoutExtension(path); + var name = Path.GetFileNameWithoutExtension(path); int? year = null; @@ -106,7 +105,7 @@ namespace Emby.Naming.Video return new VideoFileInfo( path: path, - container: container, + container: container.IsEmpty ? null : container.ToString(), isStub: isStub, name: name, year: year, @@ -125,8 +124,8 @@ namespace Emby.Naming.Video /// True if is video file. public bool IsVideoFile(string path) { - var extension = Path.GetExtension(path) ?? string.Empty; - return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + var extension = Path.GetExtension(path.AsSpan()); + return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } /// @@ -136,8 +135,8 @@ namespace Emby.Naming.Video /// True if is video file stub. public bool IsStubFile(string path) { - var extension = Path.GetExtension(path) ?? string.Empty; - return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + var extension = Path.GetExtension(path.AsSpan()); + return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } /// @@ -146,7 +145,7 @@ namespace Emby.Naming.Video /// Raw name. /// Clean name. /// True if cleaning of name was successful. - public bool TryCleanString(string name, out ReadOnlySpan newName) + public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan newName) { return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); } diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs index a602b72213..ec3490e23b 100644 --- a/Emby.Notifications/CoreNotificationTypes.cs +++ b/Emby.Notifications/CoreNotificationTypes.cs @@ -75,10 +75,6 @@ namespace Emby.Notifications Type = NotificationType.VideoPlaybackStopped.ToString() }, new NotificationTypeInfo - { - Type = NotificationType.CameraImageUploaded.ToString() - }, - new NotificationTypeInfo { Type = NotificationType.UserLockedOut.ToString() }, @@ -114,10 +110,6 @@ namespace Emby.Notifications { note.Category = _localization.GetLocalizedString("Plugin"); } - else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1) - { - note.Category = _localization.GetLocalizedString("Sync"); - } else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1) { note.Category = _localization.GetLocalizedString("User"); diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 16ee918c46..5a2aea6423 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -11,6 +11,8 @@ true true enable + AllEnabledByDefault + ../jellyfin.ruleset @@ -25,14 +27,9 @@ - - - ../jellyfin.ruleset - - diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 62e33e6c44..2b66181599 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -24,18 +24,15 @@ true true enable + AllEnabledByDefault + ../jellyfin.ruleset - - - ../jellyfin.ruleset - - diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 660bbb2deb..6edfad575a 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - DataPath = Path.Combine(ProgramDataPath, "data"); + _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } /// @@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase /// Gets the folder path to the data directory. /// /// The data directory. - public string DataPath - { - get => _dataPath; - private set => _dataPath = Directory.CreateDirectory(value).FullName; - } + public string DataPath => _dataPath; /// public string VirtualDataPath => "%AppDataPath%"; diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 4f72c8ce15..4c442a4736 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase private readonly ConcurrentDictionary _configurations = new ConcurrentDictionary(); + /// + /// The _configuration sync lock. + /// + private readonly object _configurationSyncLock = new object(); + private ConfigurationStore[] _configurationStores = Array.Empty(); private IConfigurationFactory[] _configurationFactories = Array.Empty(); @@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase /// private bool _configurationLoaded; - /// - /// The _configuration sync lock. - /// - private readonly object _configurationSyncLock = new object(); - /// /// The _configuration. /// diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index 77819c7649..0308a68e42 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -1,9 +1,6 @@ -#nullable enable - using System; using System.IO; using System.Linq; -using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Serialization; namespace Emby.Server.Implementations.AppBase @@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase } catch (Exception) { - configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type)); + // Note: CreateInstance returns null for Nullable, e.g. CreateInstance(typeof(int?)) returns null. + configuration = Activator.CreateInstance(type)!; } using var stream = new MemoryStream(buffer?.Length ?? 0); @@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase Directory.CreateDirectory(directory); // Save it after load in case we got new items - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { fs.Write(newBytes, 0, newBytesLen); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d74ea03520..82995deb30 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1,16 +1,17 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading; using System.Threading.Tasks; using Emby.Dlna; @@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; +using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; using Jellyfin.Networking.Configuration; @@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; @@ -116,10 +119,12 @@ namespace Emby.Server.Implementations private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private readonly IFileSystem _fileSystemManager; + private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; private readonly IStartupOptions _startupOptions; + private readonly IPluginManager _pluginManager; + private List _creatingInstances; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; private string[] _urlPrefixes; @@ -181,16 +186,6 @@ namespace Emby.Server.Implementations protected IServiceCollection ServiceCollection { get; } - private IPlugin[] _plugins; - - private IReadOnlyList _pluginsManifests; - - /// - /// Gets the plugins. - /// - /// The plugins. - public IReadOnlyList Plugins => _plugins; - /// /// Gets the logger factory. /// @@ -217,7 +212,7 @@ namespace Emby.Server.Implementations /// Gets or sets the configuration manager. /// /// The configuration manager. - protected IConfigurationManager ConfigurationManager { get; set; } + public ServerConfigurationManager ConfigurationManager { get; set; } /// /// Gets or sets the service provider. @@ -235,10 +230,9 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// - /// Gets the server configuration manager. + /// Gets the value of the PublishedServerUrl setting. /// - /// The server configuration manager. - public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; + public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey]; /// /// Initializes a new instance of the class. @@ -246,54 +240,39 @@ namespace Emby.Server.Implementations /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// The interface. /// Instance of the interface. /// Instance of the interface. public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, + IConfiguration startupConfig, IFileSystem fileSystem, IServiceCollection serviceCollection) { - _xmlSerializer = new MyXmlSerializer(); - _jsonSerializer = new JsonSerializer(); - - ServiceCollection = serviceCollection; - ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; + _startupOptions = options; + _startupConfig = startupConfig; _fileSystemManager = fileSystem; - - ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); - // Have to migrate settings here as migration subsystem not yet initialised. - MigrateNetworkConfiguration(); - - // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised. - ConfigurationManager.RegisterConfiguration(); - NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + ServiceCollection = serviceCollection; Logger = LoggerFactory.CreateLogger(); - - _startupOptions = options; - - // Initialize runtime stat collection - if (ServerConfigurationManager.Configuration.EnableMetrics) - { - DotNetRuntimeStatsBuilder.Default().StartCollecting(); - } - fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - CertificateInfo = new CertificateInfo - { - Path = ServerConfigurationManager.Configuration.CertificatePath, - Password = ServerConfigurationManager.Configuration.CertificatePassword - }; - Certificate = GetCertificate(CertificateInfo); - ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; + + _xmlSerializer = new MyXmlSerializer(); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + _pluginManager = new PluginManager( + LoggerFactory.CreateLogger(), + this, + ConfigurationManager.Configuration, + ApplicationPaths.PluginsPath, + ApplicationVersion); } /// @@ -306,9 +285,9 @@ namespace Emby.Server.Implementations if (!File.Exists(path)) { var networkSettings = new NetworkConfiguration(); - ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); + ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings); _xmlSerializer.SerializeToFile(networkSettings, path); - Logger?.LogDebug("Successfully migrated network settings."); + Logger.LogDebug("Successfully migrated network settings."); } } @@ -358,10 +337,7 @@ namespace Emby.Server.Implementations { get { - if (_deviceId == null) - { - _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); - } + _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory); return _deviceId.Value; } @@ -381,7 +357,7 @@ namespace Emby.Server.Implementations /// /// Creates an instance of type and resolves all constructor dependencies. /// - /// /// The type. + /// The type. /// T. public T CreateInstance() => ActivatorUtilities.CreateInstance(ServiceProvider); @@ -393,16 +369,38 @@ namespace Emby.Server.Implementations /// System.Object. protected object CreateInstanceSafe(Type type) { + _creatingInstances ??= new List(); + + if (_creatingInstances.IndexOf(type) != -1) + { + Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName); + foreach (var entry in _creatingInstances) + { + Logger.LogError("Called from: {TypeName}", entry.FullName); + } + + _pluginManager.FailPlugin(type.Assembly); + + throw new ExternalException("DI Loop detected."); + } + try { + _creatingInstances.Add(type); Logger.LogDebug("Creating instance of {Type}", type); return ActivatorUtilities.CreateInstance(ServiceProvider, type); } catch (Exception ex) { Logger.LogError(ex, "Error creating {Type}", type); + // If this is a plugin fail it. + _pluginManager.FailPlugin(type.Assembly); return null; } + finally + { + _creatingInstances.Remove(type); + } } /// @@ -412,11 +410,7 @@ namespace Emby.Server.Implementations /// ``0. public T Resolve() => ServiceProvider.GetService(); - /// - /// Gets the export types. - /// - /// The type. - /// IEnumerable{Type}. + /// public IEnumerable GetExportTypes() { var currentType = typeof(T); @@ -445,17 +439,40 @@ namespace Emby.Server.Implementations return parts; } + /// + public IReadOnlyCollection GetExports(CreationDelegateFactory defaultFunc, bool manageLifetime = true) + { + // Convert to list so this isn't executed for each iteration + var parts = GetExportTypes() + .Select(i => defaultFunc(i)) + .Where(i => i != null) + .Cast() + .ToList(); + + if (manageLifetime) + { + lock (_disposableParts) + { + _disposableParts.AddRange(parts.OfType()); + } + } + + return parts; + } + /// /// Runs the startup tasks. /// /// . - public async Task RunStartupTasksAsync() + public async Task RunStartupTasksAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); Logger.LogInformation("Running startup tasks"); Resolve().AddTasks(GetExports(false)); ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; + ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated; _mediaEncoder.SetFFmpegPath(); @@ -463,14 +480,21 @@ namespace Emby.Server.Implementations var entryPoints = GetExports(); + cancellationToken.ThrowIfCancellationRequested(); + var stopWatch = new Stopwatch(); stopWatch.Start(); + await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false); Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); CoreStartupHasCompleted = true; + + cancellationToken.ThrowIfCancellationRequested(); + stopWatch.Restart(); + await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); stopWatch.Stop(); @@ -494,7 +518,21 @@ namespace Emby.Server.Implementations /// public void Init() { - var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); + DiscoverTypes(); + + ConfigurationManager.AddParts(GetExports()); + + // Have to migrate settings here as migration subsystem not yet initialised. + MigrateNetworkConfiguration(); + NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger()); + + // Initialize runtime stat collection + if (ConfigurationManager.Configuration.EnableMetrics) + { + DotNetRuntimeStatsBuilder.Default().StartCollecting(); + } + + var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); HttpPort = networkConfiguration.HttpServerPortNumber; HttpsPort = networkConfiguration.HttpsPortNumber; @@ -505,11 +543,16 @@ namespace Emby.Server.Implementations HttpsPort = NetworkConfiguration.DefaultHttpsPort; } - DiscoverTypes(); + CertificateInfo = new CertificateInfo + { + Path = networkConfiguration.CertificatePath, + Password = networkConfiguration.CertificatePassword + }; + Certificate = GetCertificate(CertificateInfo); RegisterServices(); - RegisterPluginServices(); + _pluginManager.RegisterServices(ServiceCollection); } /// @@ -521,13 +564,12 @@ namespace Emby.Server.Implementations ServiceCollection.AddMemoryCache(); - ServiceCollection.AddSingleton(ConfigurationManager); + ServiceCollection.AddSingleton(ConfigurationManager); + ServiceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton(this); - + ServiceCollection.AddSingleton(_pluginManager); ServiceCollection.AddSingleton(ApplicationPaths); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(); @@ -550,8 +592,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(this); ServiceCollection.AddSingleton(ApplicationPaths); - ServiceCollection.AddSingleton(ServerConfigurationManager); - ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); @@ -563,12 +603,8 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - - // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); @@ -633,14 +669,14 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); ServiceCollection.AddScoped(); ServiceCollection.AddScoped(); ServiceCollection.AddScoped(); + + ServiceCollection.AddSingleton(); } /// @@ -714,7 +750,7 @@ namespace Emby.Server.Implementations // Don't use an empty string password var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; - var localCert = new X509Certificate2(certificateLocation, password); + var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet); // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; if (!localCert.HasPrivateKey) { @@ -738,7 +774,7 @@ namespace Emby.Server.Implementations { // For now there's no real way to inject these properly BaseItem.Logger = Resolve>(); - BaseItem.ConfigurationManager = ServerConfigurationManager; + BaseItem.ConfigurationManager = ConfigurationManager; BaseItem.LibraryManager = Resolve(); BaseItem.ProviderManager = Resolve(); BaseItem.LocalizationManager = Resolve(); @@ -752,7 +788,6 @@ namespace Emby.Server.Implementations UserView.CollectionManager = Resolve(); BaseItem.MediaSourceManager = Resolve(); CollectionFolder.XmlSerializer = _xmlSerializer; - CollectionFolder.JsonSerializer = Resolve(); CollectionFolder.ApplicationHost = this; } @@ -761,41 +796,13 @@ namespace Emby.Server.Implementations /// private void FindParts() { - if (!ServerConfigurationManager.Configuration.IsPortAuthorized) + if (!ConfigurationManager.Configuration.IsPortAuthorized) { - ServerConfigurationManager.Configuration.IsPortAuthorized = true; + ConfigurationManager.Configuration.IsPortAuthorized = true; ConfigurationManager.SaveConfiguration(); } - ConfigurationManager.AddParts(GetExports()); - _plugins = GetExports() - .Where(i => i != null) - .ToArray(); - - if (Plugins != null) - { - foreach (var plugin in Plugins) - { - if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin) - { - // Ensure the version number matches the Plugin Manifest information. - foreach (var item in _pluginsManifests) - { - if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase)) - { - // Update version number to that of the manifest. - assemblyPlugin.SetAttributes( - plugin.AssemblyFilePath, - Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)), - item.Version); - break; - } - } - } - - Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version); - } - } + _pluginManager.CreatePlugins(); _urlPrefixes = GetUrlPrefixes().ToArray(); @@ -834,22 +841,6 @@ namespace Emby.Server.Implementations _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); } - private void RegisterPluginServices() - { - foreach (var pluginServiceRegistrator in GetExportTypes()) - { - try - { - var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator); - instance.RegisterServices(ServiceCollection); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly); - } - } - } - private IEnumerable GetTypes(IEnumerable assemblies) { foreach (var ass in assemblies) @@ -862,11 +853,13 @@ namespace Emby.Server.Implementations catch (FileNotFoundException ex) { Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); + _pluginManager.FailPlugin(ass); continue; } catch (TypeLoadException ex) { Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); + _pluginManager.FailPlugin(ass); continue; } @@ -912,19 +905,19 @@ namespace Emby.Server.Implementations protected void OnConfigurationUpdated(object sender, EventArgs e) { var requiresRestart = false; + var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); // Don't do anything if these haven't been set yet if (HttpPort != 0 && HttpsPort != 0) { - var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); // Need to restart if ports have changed if (networkConfiguration.HttpServerPortNumber != HttpPort || networkConfiguration.HttpsPortNumber != HttpsPort) { - if (ServerConfigurationManager.Configuration.IsPortAuthorized) + if (ConfigurationManager.Configuration.IsPortAuthorized) { - ServerConfigurationManager.Configuration.IsPortAuthorized = false; - ServerConfigurationManager.SaveConfiguration(); + ConfigurationManager.Configuration.IsPortAuthorized = false; + ConfigurationManager.SaveConfiguration(); requiresRestart = true; } @@ -936,10 +929,7 @@ namespace Emby.Server.Implementations requiresRestart = true; } - var currentCertPath = CertificateInfo?.Path; - var newCertPath = ServerConfigurationManager.Configuration.CertificatePath; - - if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase)) + if (ValidateSslCertificate(networkConfiguration)) { requiresRestart = true; } @@ -952,6 +942,33 @@ namespace Emby.Server.Implementations } } + /// + /// Validates the SSL certificate. + /// + /// The new configuration. + /// The certificate path doesn't exist. + private bool ValidateSslCertificate(NetworkConfiguration networkConfig) + { + var newPath = networkConfig.CertificatePath; + + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal)) + { + if (File.Exists(newPath)) + { + return true; + } + + throw new FileNotFoundException( + string.Format( + CultureInfo.InvariantCulture, + "Certificate file '{0}' does not exist.", + newPath)); + } + + return false; + } + /// /// Notifies that the kernel that a change has been made that requires a restart. /// @@ -1005,129 +1022,15 @@ namespace Emby.Server.Implementations protected abstract void RestartInternal(); - /// - public IEnumerable GetLocalPlugins(string path, bool cleanup = true) - { - var minimumVersion = new Version(0, 0, 0, 1); - var versions = new List(); - if (!Directory.Exists(path)) - { - // Plugin path doesn't exist, don't try to enumerate subfolders. - return Enumerable.Empty(); - } - - var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); - - foreach (var dir in directories) - { - try - { - var metafile = Path.Combine(dir, "meta.json"); - if (File.Exists(metafile)) - { - var manifest = _jsonSerializer.DeserializeFromFile(metafile); - - if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) - { - targetAbi = minimumVersion; - } - - if (!Version.TryParse(manifest.Version, out var version)) - { - version = minimumVersion; - } - - if (ApplicationVersion >= targetAbi) - { - // Only load Plugins if the plugin is built for this version or below. - versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir)); - } - } - else - { - // No metafile, so lets see if the folder is versioned. - metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; - - int versionIndex = dir.LastIndexOf('_'); - if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion)) - { - // Versioned folder. - versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir)); - } - else - { - // Un-versioned folder - Add it under the path name and version 0.0.0.1. - versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir)); - } - } - } - catch - { - continue; - } - } - - string lastName = string.Empty; - versions.Sort(LocalPlugin.Compare); - // Traverse backwards through the list. - // The first item will be the latest version. - for (int x = versions.Count - 1; x >= 0; x--) - { - if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase)) - { - versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories)); - lastName = versions[x].Name; - continue; - } - - if (!string.IsNullOrEmpty(lastName) && cleanup) - { - // Attempt a cleanup of old folders. - try - { - Logger.LogDebug("Deleting {Path}", versions[x].Path); - Directory.Delete(versions[x].Path, true); - } - catch (Exception e) - { - Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path); - } - - versions.RemoveAt(x); - } - } - - return versions; - } - /// /// Gets the composable part assemblies. /// /// IEnumerable{Assembly}. protected IEnumerable GetComposablePartAssemblies() { - if (Directory.Exists(ApplicationPaths.PluginsPath)) + foreach (var p in _pluginManager.LoadAssemblies()) { - _pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList(); - foreach (var plugin in _pluginsManifests) - { - foreach (var file in plugin.DllFiles) - { - Assembly plugAss; - try - { - plugAss = Assembly.LoadFrom(file); - } - catch (FileLoadException ex) - { - Logger.LogError(ex, "Failed to load assembly {Path}", file); - continue; - } - - Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); - yield return plugAss; - } - } + yield return p; } // Include composable parts in the Model assembly @@ -1230,16 +1133,16 @@ namespace Emby.Server.Implementations } /// - public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps; + public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps; /// public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) { // Published server ends with a / - if (_startupOptions.PublishedServerUrl != null) + if (!string.IsNullOrEmpty(PublishedServerUrl)) { // Published server ends with a '/', so we need to remove it. - return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + return PublishedServerUrl.Trim('/'); } string smart = NetManager.GetBindInterface(ipAddress, out port); @@ -1256,10 +1159,10 @@ namespace Emby.Server.Implementations public string GetSmartApiUrl(HttpRequest request, int? port = null) { // Published server ends with a / - if (_startupOptions.PublishedServerUrl != null) + if (!string.IsNullOrEmpty(PublishedServerUrl)) { // Published server ends with a '/', so we need to remove it. - return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + return PublishedServerUrl.Trim('/'); } string smart = NetManager.GetBindInterface(request, out port); @@ -1276,10 +1179,10 @@ namespace Emby.Server.Implementations public string GetSmartApiUrl(string hostname, int? port = null) { // Published server ends with a / - if (_startupOptions.PublishedServerUrl != null) + if (!string.IsNullOrEmpty(PublishedServerUrl)) { // Published server ends with a '/', so we need to remove it. - return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + return PublishedServerUrl.Trim('/'); } string smart = NetManager.GetBindInterface(hostname, out port); @@ -1314,14 +1217,14 @@ namespace Emby.Server.Implementations Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Host = host, Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), - Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl + Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl }.ToString().TrimEnd('/'); } public string FriendlyName => - string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) + string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName) ? Environment.MachineName - : ServerConfigurationManager.Configuration.ServerName; + : ConfigurationManager.Configuration.ServerName; /// /// Shuts down. @@ -1369,17 +1272,6 @@ namespace Emby.Server.Implementations } } - /// - /// Removes the plugin. - /// - /// The plugin. - public void RemovePlugin(IPlugin plugin) - { - var list = _plugins.ToList(); - list.Remove(plugin); - _plugins = list.ToArray(); - } - public IEnumerable GetApiPluginAssemblies() { var assemblies = _allConcreteTypes diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 57684a4298..448f124034 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1,13 +1,17 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -21,7 +25,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; @@ -44,10 +47,10 @@ namespace Emby.Server.Implementations.Channels private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; private readonly IProviderManager _providerManager; private readonly IMemoryCache _memoryCache; private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; /// /// Initializes a new instance of the class. @@ -59,7 +62,6 @@ namespace Emby.Server.Implementations.Channels /// The server configuration manager. /// The filesystem. /// The user data manager. - /// The JSON serializer. /// The provider manager. /// The memory cache. public ChannelManager( @@ -70,7 +72,6 @@ namespace Emby.Server.Implementations.Channels IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, - IJsonSerializer jsonSerializer, IProviderManager providerManager, IMemoryCache memoryCache) { @@ -81,7 +82,6 @@ namespace Emby.Server.Implementations.Channels _config = config; _fileSystem = fileSystem; _userDataManager = userDataManager; - _jsonSerializer = jsonSerializer; _providerManager = providerManager; _memoryCache = memoryCache; } @@ -337,21 +337,23 @@ namespace Emby.Server.Implementations.Channels return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result; } - private List GetSavedMediaSources(BaseItem item) + private MediaSourceInfo[] GetSavedMediaSources(BaseItem item) { var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); try { - return _jsonSerializer.DeserializeFromFile>(path) ?? new List(); + var bytes = File.ReadAllBytes(path); + return JsonSerializer.Deserialize(bytes, _jsonOptions) + ?? Array.Empty(); } catch { - return new List(); + return Array.Empty(); } } - private void SaveMediaSources(BaseItem item, List mediaSources) + private async Task SaveMediaSources(BaseItem item, List mediaSources) { var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); @@ -370,7 +372,8 @@ namespace Emby.Server.Implementations.Channels Directory.CreateDirectory(Path.GetDirectoryName(path)); - _jsonSerializer.SerializeToFile(mediaSources, path); + await using FileStream createStream = File.Create(path); + await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); } /// @@ -812,7 +815,8 @@ namespace Emby.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - var cachedResult = _jsonSerializer.DeserializeFromFile(cachePath); + await using FileStream jsonStream = File.OpenRead(cachePath); + var cachedResult = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (cachedResult != null) { return null; @@ -834,7 +838,8 @@ namespace Emby.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - var cachedResult = _jsonSerializer.DeserializeFromFile(cachePath); + await using FileStream jsonStream = File.OpenRead(cachePath); + var cachedResult = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (cachedResult != null) { return null; @@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Channels throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); } - CacheResponse(result, cachePath); + await CacheResponse(result, cachePath); return result; } @@ -875,13 +880,14 @@ namespace Emby.Server.Implementations.Channels } } - private void CacheResponse(object result, string path) + private async Task CacheResponse(object result, string path) { try { Directory.CreateDirectory(Path.GetDirectoryName(path)); - _jsonSerializer.SerializeToFile(result, path); + await using FileStream createStream = File.Create(path); + await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false); } catch (Exception ex) { @@ -1176,11 +1182,11 @@ namespace Emby.Server.Implementations.Channels { if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol) { - SaveMediaSources(item, new List()); + await SaveMediaSources(item, new List()).ConfigureAwait(false); } else { - SaveMediaSources(item, info.MediaSources); + await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index c69a07e83e..ca84094024 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections return null; }) .Where(i => i != null) - .GroupBy(x => x.Id) + .GroupBy(x => x!.Id) // We removed the null values .Select(x => x.First()) - .ToList(); + .ToList()!; // Again... the list doesn't contain any null values } /// diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 3011a37e31..82d80fc83c 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -1,6 +1,7 @@ +#nullable disable + using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -8,11 +9,9 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections var name = _localizationManager.GetLocalizedString("Collections"); - await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false); + await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false); return FindFolders(path).First(); } @@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections private IEnumerable GetCollections(User user) { - var folder = GetCollectionsFolder(false).Result; + var folder = GetCollectionsFolder(false).GetAwaiter().GetResult(); return folder == null ? Enumerable.Empty() @@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections parentFolder.AddChild(collection, CancellationToken.None); - if (options.ItemIdList.Length > 0) + if (options.ItemIdList.Count > 0) { await AddToCollectionAsync( collection.Id, @@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections if (fireEvent) { - ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs - { - Collection = collection, - ItemsChanged = itemList - }); + ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList)); } } } @@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections }, RefreshPriority.High); - ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs - { - Collection = collection, - ItemsChanged = itemList - }); + ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList)); } /// @@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections { var results = new Dictionary(); - var allBoxsets = GetCollections(user).ToList(); + var allBoxSets = GetCollections(user).ToList(); foreach (var item in items) { - if (!(item is ISupportsBoxSetGrouping)) + if (item is not ISupportsBoxSetGrouping) { results[item.Id] = item; } @@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections { var itemId = item.Id; - var currentBoxSets = allBoxsets - .Where(i => i.ContainsLinkedChildByItemId(itemId)) - .ToList(); + var itemIsInBoxSet = false; + foreach (var boxSet in allBoxSets) + { + if (!boxSet.ContainsLinkedChildByItemId(itemId)) + { + continue; + } + + itemIsInBoxSet = true; - if (currentBoxSets.Count > 0) + results.TryAdd(boxSet.Id, boxSet); + } + + // skip any item that is in a box set + if (itemIsInBoxSet) { - foreach (var boxset in currentBoxSets) + continue; + } + + var alreadyInResults = false; + // this is kind of a performance hack because only Video has alternate versions that should be in a box set? + if (item is Video video) + { + foreach (var childId in video.GetLocalAlternateVersionIds()) { - results[boxset.Id] = boxset; + if (!results.ContainsKey(childId)) + { + continue; + } + + alreadyInResults = true; + break; } } - else + + if (!alreadyInResults) { - results[item.Id] = item; + results[itemId] = item; } } } diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index f05a30a897..ff5602f243 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Globalization; using System.IO; @@ -88,38 +90,12 @@ namespace Emby.Server.Implementations.Configuration var newConfig = (ServerConfiguration)newConfiguration; ValidateMetadataPath(newConfig); - ValidateSslCertificate(newConfig); ConfigurationUpdating?.Invoke(this, new GenericEventArgs(newConfig)); base.ReplaceConfiguration(newConfiguration); } - /// - /// Validates the SSL certificate. - /// - /// The new configuration. - /// The certificate path doesn't exist. - private void ValidateSslCertificate(BaseApplicationConfiguration newConfig) - { - var serverConfig = (ServerConfiguration)newConfig; - - var newPath = serverConfig.CertificatePath; - - if (!string.IsNullOrWhiteSpace(newPath) - && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal)) - { - if (!File.Exists(newPath)) - { - throw new FileNotFoundException( - string.Format( - CultureInfo.InvariantCulture, - "Certificate file '{0}' does not exist.", - newPath)); - } - } - } - /// /// Validates the metadata path. /// diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index cd9dbb1bda..01dc728c1c 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Emby.Server.Implementations.HttpServer; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 12a9e44e70..4a9b280852 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Security.Cryptography; diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 8c756a7f4c..6f23a0888e 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) { - if (row[1].SQLiteType != SQLiteType.Null) + if (row.TryGetString(1, out var columnName)) { - var name = row[1].ToString(); - - columnNames.Add(name); + columnNames.Add(columnName); } } diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 5c094ddd2d..afc8966f9c 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data { public class ManagedConnection : IDisposable { - private SQLiteDatabaseConnection _db; + private SQLiteDatabaseConnection? _db; private readonly SemaphoreSlim _writeLock; private bool _disposed = false; @@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data return _db.RunInTransaction(action, mode); } - public IEnumerable> Query(string sql) + public IEnumerable> Query(string sql) { return _db.Query(sql); } - public IEnumerable> Query(string sql, params object[] values) + public IEnumerable> Query(string sql, params object[] values) { return _db.Query(sql, values); } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 1af301ceb0..3289e76098 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -1,7 +1,9 @@ +#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using SQLitePCL.pretty; @@ -59,11 +61,11 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction(conn => { - conn.ExecuteAll(string.Join(";", queries)); + conn.ExecuteAll(string.Join(';', queries)); }); } - public static Guid ReadGuidFromBlob(this IResultSetValue result) + public static Guid ReadGuidFromBlob(this ResultSetValue result) { return new Guid(result.ToBlob()); } @@ -84,7 +86,7 @@ namespace Emby.Server.Implementations.Data private static string GetDateTimeKindFormat(DateTimeKind kind) => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; - public static DateTime ReadDateTime(this IResultSetValue result) + public static DateTime ReadDateTime(this ResultSetValue result) { var dateText = result.ToString(); @@ -95,58 +97,147 @@ namespace Emby.Server.Implementations.Data DateTimeStyles.None).ToUniversalTime(); } - public static DateTime? TryReadDateTime(this IResultSetValue result) + public static bool TryReadDateTime(this IReadOnlyList reader, int index, out DateTime result) { - var dateText = result.ToString(); + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + var dateText = item.ToString(); if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult)) { - return dateTimeResult.ToUniversalTime(); + result = dateTimeResult.ToUniversalTime(); + return true; + } + + result = default; + return false; + } + + public static bool TryGetGuid(this IReadOnlyList reader, int index, out Guid result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; } - return null; + result = item.ReadGuidFromBlob(); + return true; } - public static bool IsDBNull(this IReadOnlyList result, int index) + public static bool IsDbNull(this ResultSetValue result) { - return result[index].SQLiteType == SQLiteType.Null; + return result.SQLiteType == SQLiteType.Null; } - public static string GetString(this IReadOnlyList result, int index) + public static string GetString(this IReadOnlyList result, int index) { return result[index].ToString(); } - public static bool GetBoolean(this IReadOnlyList result, int index) + public static bool TryGetString(this IReadOnlyList reader, int index, out string result) + { + result = null; + var item = reader[index]; + if (item.IsDbNull()) + { + return false; + } + + result = item.ToString(); + return true; + } + + public static bool GetBoolean(this IReadOnlyList result, int index) { return result[index].ToBool(); } - public static int GetInt32(this IReadOnlyList result, int index) + public static bool TryGetBoolean(this IReadOnlyList reader, int index, out bool result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToBool(); + return true; + } + + public static bool TryGetInt32(this IReadOnlyList reader, int index, out int result) { - return result[index].ToInt(); + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToInt(); + return true; } - public static long GetInt64(this IReadOnlyList result, int index) + public static long GetInt64(this IReadOnlyList result, int index) { return result[index].ToInt64(); } - public static float GetFloat(this IReadOnlyList result, int index) + public static bool TryGetInt64(this IReadOnlyList reader, int index, out long result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToInt64(); + return true; + } + + public static bool TryGetSingle(this IReadOnlyList reader, int index, out float result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToFloat(); + return true; + } + + public static bool TryGetDouble(this IReadOnlyList reader, int index, out double result) { - return result[index].ToFloat(); + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToDouble(); + return true; } - public static Guid GetGuid(this IReadOnlyList result, int index) + public static Guid GetGuid(this IReadOnlyList result, int index) { return result[index].ReadGuidFromBlob(); } + [Conditional("DEBUG")] private static void CheckName(string name) { -#if DEBUG throw new ArgumentException("Invalid param name: " + name, nameof(name)); -#endif } public static void TryBind(this IStatement statement, string name, double value) @@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data } } - public static IEnumerable> ExecuteQuery(this IStatement statement) + public static IEnumerable> ExecuteQuery(this IStatement statement) { while (statement.MoveNext()) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 6e1f2feae4..2d060dd652 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,6 +1,9 @@ +#nullable disable + #pragma warning disable CS1591 using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -88,7 +91,7 @@ namespace Emby.Server.Implementations.Data _imageProcessor = imageProcessor; _typeMapper = new TypeMapper(); - _jsonOptions = JsonDefaults.GetOptions(); + _jsonOptions = JsonDefaults.Options; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } @@ -502,7 +505,7 @@ namespace Emby.Server.Implementations.Data using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) { saveImagesStatement.TryBind("@Id", item.Id.ToByteArray()); - saveImagesStatement.TryBind("@Images", SerializeImages(item)); + saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); saveImagesStatement.MoveNext(); } @@ -687,7 +690,7 @@ namespace Emby.Server.Implementations.Data if (item.Genres.Length > 0) { - saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres)); + saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); } else { @@ -749,7 +752,7 @@ namespace Emby.Server.Implementations.Data if (item.LockedFields.Length > 0) { - saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields)); + saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); } else { @@ -758,7 +761,7 @@ namespace Emby.Server.Implementations.Data if (item.Studios.Length > 0) { - saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios)); + saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); } else { @@ -785,7 +788,7 @@ namespace Emby.Server.Implementations.Data if (item.Tags.Length > 0) { - saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags)); + saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); } else { @@ -807,7 +810,7 @@ namespace Emby.Server.Implementations.Data if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) { - saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes)); + saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); } else { @@ -897,12 +900,12 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); saveItemStatement.TryBind("@Tagline", item.Tagline); - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item)); - saveItemStatement.TryBind("@Images", SerializeImages(item)); + saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); + saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); if (item.ProductionLocations.Length > 0) { - saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations)); + saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); } else { @@ -911,7 +914,7 @@ namespace Emby.Server.Implementations.Data if (item.ExtraIds.Length > 0) { - saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds)); + saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); } else { @@ -931,7 +934,7 @@ namespace Emby.Server.Implementations.Data string artists = null; if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) { - artists = string.Join("|", hasArtists.Artists); + artists = string.Join('|', hasArtists.Artists); } saveItemStatement.TryBind("@Artists", artists); @@ -940,7 +943,7 @@ namespace Emby.Server.Implementations.Data if (item is IHasAlbumArtist hasAlbumArtists && hasAlbumArtists.AlbumArtists.Count > 0) { - albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists); + albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); } saveItemStatement.TryBind("@AlbumArtists", albumArtists); @@ -968,10 +971,10 @@ namespace Emby.Server.Implementations.Data saveItemStatement.MoveNext(); } - private static string SerializeProviderIds(BaseItem item) + internal static string SerializeProviderIds(Dictionary providerIds) { StringBuilder str = new StringBuilder(); - foreach (var i in item.ProviderIds) + foreach (var i in providerIds) { // Ideally we shouldn't need this IsNullOrWhiteSpace check, // but we're seeing some cases of bad data slip through @@ -995,35 +998,25 @@ namespace Emby.Server.Implementations.Data return str.ToString(); } - private static void DeserializeProviderIds(string value, BaseItem item) + internal static void DeserializeProviderIds(string value, IHasProviderIds item) { if (string.IsNullOrWhiteSpace(value)) { return; } - if (item.ProviderIds.Count > 0) - { - return; - } - - var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries); - - foreach (var part in parts) + foreach (var part in value.SpanSplit('|')) { - var idParts = part.Split('='); - - if (idParts.Length == 2) + var providerDelimiterIndex = part.IndexOf('='); + if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('=')) { - item.SetProviderId(idParts[0], idParts[1]); + item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString()); } } } - private string SerializeImages(BaseItem item) + internal string SerializeImages(ItemImageInfo[] images) { - var images = item.ImageInfos; - if (images.Length == 0) { return null; @@ -1045,21 +1038,15 @@ namespace Emby.Server.Implementations.Data return str.ToString(); } - private void DeserializeImages(string value, BaseItem item) + internal ItemImageInfo[] DeserializeImages(string value) { if (string.IsNullOrWhiteSpace(value)) { - return; + return Array.Empty(); } - if (item.ImageInfos.Length > 0) - { - return; - } - - var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries); var list = new List(); - foreach (var part in parts) + foreach (var part in value.SpanSplit('|')) { var image = ItemImageInfoFromValueString(part); @@ -1069,15 +1056,14 @@ namespace Emby.Server.Implementations.Data } } - item.ImageInfos = list.ToArray(); + return list.ToArray(); } - public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) + private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) { const char Delimiter = '*'; var path = image.Path ?? string.Empty; - var hash = image.BlurHash ?? string.Empty; bldr.Append(GetPathToSave(path)) .Append(Delimiter) @@ -1087,48 +1073,105 @@ namespace Emby.Server.Implementations.Data .Append(Delimiter) .Append(image.Width) .Append(Delimiter) - .Append(image.Height) - .Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace('*', '/').Replace('|', '\\')); + .Append(image.Height); + + var hash = image.BlurHash; + if (!string.IsNullOrEmpty(hash)) + { + bldr.Append(Delimiter) + // Replace delimiters with other characters. + // This can be removed when we migrate to a proper DB. + .Append(hash.Replace('*', '/').Replace('|', '\\')); + } } - public ItemImageInfo ItemImageInfoFromValueString(string value) + internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value) { - var parts = value.Split('*', StringSplitOptions.None); + var nextSegment = value.IndexOf('*'); + if (nextSegment == -1) + { + return null; + } - if (parts.Length < 3) + ReadOnlySpan path = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1) { return null; } - var image = new ItemImageInfo(); + ReadOnlySpan dateModified = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan imageType = value[..nextSegment]; - image.Path = RestorePath(parts[0]); + var image = new ItemImageInfo + { + Path = RestorePath(path.ToString()) + }; - if (long.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)) + if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)) { image.DateModified = new DateTime(ticks, DateTimeKind.Utc); } - if (Enum.TryParse(parts[2], true, out ImageType type)) + if (Enum.TryParse(imageType.ToString(), true, out ImageType type)) { image.Type = type; } - if (parts.Length >= 5) + // Optional parameters: width*height*blurhash + if (nextSegment + 1 < value.Length - 1) { - if (int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(parts[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1 || nextSegment == value.Length) + { + return image; + } + + ReadOnlySpan widthSpan = value[..nextSegment]; + + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan heightSpan = value[..nextSegment]; + + if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) + && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) { image.Width = width; image.Height = height; } - if (parts.Length >= 6) + if (nextSegment < value.Length - 1) { - image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|'); + value = value[(nextSegment + 1)..]; + var length = value.Length; + + Span blurHashSpan = stackalloc char[length]; + for (int i = 0; i < length; i++) + { + var c = value[i]; + blurHashSpan[i] = c switch + { + '/' => '*', + '\\' => '|', + _ => c + }; + } + + image.BlurHash = new string(blurHashSpan); } } @@ -1241,12 +1284,12 @@ namespace Emby.Server.Implementations.Data return true; } - private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query) + private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query) { return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query)); } - private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) + private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) { var typeString = reader.GetString(0); @@ -1291,27 +1334,30 @@ namespace Emby.Server.Implementations.Data if (queryHasStartDate) { - if (!reader.IsDBNull(index)) + if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate)) { - if (item is IHasStartDate hasStartDate) - { - hasStartDate.StartDate = reader[index].ReadDateTime(); - } + hasStartDate.StartDate = startDate; } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var endDate)) { - item.EndDate = reader[index].TryReadDateTime(); + item.EndDate = endDate; } - index++; - - if (!reader.IsDBNull(index)) + var channelId = reader[index]; + if (!channelId.IsDbNull()) { - item.ChannelId = new Guid(reader.GetString(index)); + if (!Utf8Parser.TryParse(channelId.ToBlob(), out Guid value, out _, standardFormat: 'N')) + { + var str = reader.GetString(index); + Logger.LogWarning("{ChannelId} isn't in the expected format", str); + value = new Guid(str); + } + + item.ChannelId = value; } index++; @@ -1320,33 +1366,25 @@ namespace Emby.Server.Implementations.Data { if (item is IHasProgramAttributes hasProgramAttributes) { - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isMovie)) { - hasProgramAttributes.IsMovie = reader.GetBoolean(index); + hasProgramAttributes.IsMovie = isMovie; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isSeries)) { - hasProgramAttributes.IsSeries = reader.GetBoolean(index); + hasProgramAttributes.IsSeries = isSeries; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var episodeTitle)) { - hasProgramAttributes.EpisodeTitle = reader.GetString(index); + hasProgramAttributes.EpisodeTitle = episodeTitle; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isRepeat)) { - hasProgramAttributes.IsRepeat = reader.GetBoolean(index); + hasProgramAttributes.IsRepeat = isRepeat; } - - index++; } else { @@ -1354,242 +1392,190 @@ namespace Emby.Server.Implementations.Data } } - if (!reader.IsDBNull(index)) + if (reader.TryGetSingle(index++, out var communityRating)) { - item.CommunityRating = reader.GetFloat(index); + item.CommunityRating = communityRating; } - index++; - if (HasField(query, ItemFields.CustomRating)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var customRating)) { - item.CustomRating = reader.GetString(index); + item.CustomRating = customRating; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var indexNumber)) { - item.IndexNumber = reader.GetInt32(index); + item.IndexNumber = indexNumber; } - index++; - if (HasField(query, ItemFields.Settings)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isLocked)) { - item.IsLocked = reader.GetBoolean(index); + item.IsLocked = isLocked; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var preferredMetadataLanguage)) { - item.PreferredMetadataLanguage = reader.GetString(index); + item.PreferredMetadataLanguage = preferredMetadataLanguage; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var preferredMetadataCountryCode)) { - item.PreferredMetadataCountryCode = reader.GetString(index); + item.PreferredMetadataCountryCode = preferredMetadataCountryCode; } - - index++; } if (HasField(query, ItemFields.Width)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var width)) { - item.Width = reader.GetInt32(index); + item.Width = width; } - - index++; } if (HasField(query, ItemFields.Height)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var height)) { - item.Height = reader.GetInt32(index); + item.Height = height; } - - index++; } if (HasField(query, ItemFields.DateLastRefreshed)) { - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateLastRefreshed)) { - item.DateLastRefreshed = reader[index].ReadDateTime(); + item.DateLastRefreshed = dateLastRefreshed; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var name)) { - item.Name = reader.GetString(index); + item.Name = name; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var restorePath)) { - item.Path = RestorePath(reader.GetString(index)); + item.Path = RestorePath(restorePath); } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var premiereDate)) { - item.PremiereDate = reader[index].TryReadDateTime(); + item.PremiereDate = premiereDate; } - index++; - if (HasField(query, ItemFields.Overview)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var overview)) { - item.Overview = reader.GetString(index); + item.Overview = overview; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var parentIndexNumber)) { - item.ParentIndexNumber = reader.GetInt32(index); + item.ParentIndexNumber = parentIndexNumber; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var productionYear)) { - item.ProductionYear = reader.GetInt32(index); + item.ProductionYear = productionYear; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var officialRating)) { - item.OfficialRating = reader.GetString(index); + item.OfficialRating = officialRating; } - index++; - if (HasField(query, ItemFields.SortName)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var forcedSortName)) { - item.ForcedSortName = reader.GetString(index); + item.ForcedSortName = forcedSortName; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt64(index++, out var runTimeTicks)) { - item.RunTimeTicks = reader.GetInt64(index); + item.RunTimeTicks = runTimeTicks; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetInt64(index++, out var size)) { - item.Size = reader.GetInt64(index); + item.Size = size; } - index++; - if (HasField(query, ItemFields.DateCreated)) { - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateCreated)) { - item.DateCreated = reader[index].ReadDateTime(); + item.DateCreated = dateCreated; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateModified)) { - item.DateModified = reader[index].ReadDateTime(); + item.DateModified = dateModified; } - index++; - - item.Id = reader.GetGuid(index); - index++; + item.Id = reader.GetGuid(index++); if (HasField(query, ItemFields.Genres)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var genres)) { - item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index++, out var parentId)) { - item.ParentId = reader.GetGuid(index); + item.ParentId = parentId; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var audioString)) { - if (Enum.TryParse(reader.GetString(index), true, out ProgramAudio audio)) + // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916 + if (Enum.TryParse(audioString, true, out ProgramAudio audio)) { item.Audio = audio; } } - index++; - // TODO: Even if not needed by apps, the server needs it internally // But get this excluded from contexts where it is not needed if (hasServiceName) { if (item is LiveTvChannel liveTvChannel) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var serviceName)) { - liveTvChannel.ServiceName = reader.GetString(index); + liveTvChannel.ServiceName = serviceName; } } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isInMixedFolder)) { - item.IsInMixedFolder = reader.GetBoolean(index); + item.IsInMixedFolder = isInMixedFolder; } - index++; - if (HasField(query, ItemFields.DateLastSaved)) { - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateLastSaved)) { - item.DateLastSaved = reader[index].ReadDateTime(); + item.DateLastSaved = dateLastSaved; } - - index++; } if (HasField(query, ItemFields.Settings)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var lockedFields)) { IEnumerable GetLockedFields(string s) { @@ -1602,37 +1588,31 @@ namespace Emby.Server.Implementations.Data } } - item.LockedFields = GetLockedFields(reader.GetString(index)).ToArray(); + item.LockedFields = GetLockedFields(lockedFields).ToArray(); } - - index++; } if (HasField(query, ItemFields.Studios)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var studios)) { - item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } if (HasField(query, ItemFields.Tags)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var tags)) { - item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } if (hasTrailerTypes) { if (item is Trailer trailer) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var trailerTypes)) { IEnumerable GetTrailerTypes(string s) { @@ -1645,7 +1625,7 @@ namespace Emby.Server.Implementations.Data } } - trailer.TrailerTypes = GetTrailerTypes(reader.GetString(index)).ToArray(); + trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray(); } } @@ -1654,19 +1634,17 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.OriginalTitle)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var originalTitle)) { - item.OriginalTitle = reader.GetString(index); + item.OriginalTitle = originalTitle; } - - index++; } if (item is Video video) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var primaryVersionId)) { - video.PrimaryVersionId = reader.GetString(index); + video.PrimaryVersionId = primaryVersionId; } } @@ -1674,40 +1652,34 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.DateLastMediaAdded)) { - if (item is Folder folder && !reader.IsDBNull(index)) + if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded)) { - folder.DateLastMediaAdded = reader[index].TryReadDateTime(); + folder.DateLastMediaAdded = dateLastMediaAdded; } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var album)) { - item.Album = reader.GetString(index); + item.Album = album; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetSingle(index++, out var criticRating)) { - item.CriticRating = reader.GetFloat(index); + item.CriticRating = criticRating; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isVirtualItem)) { - item.IsVirtualItem = reader.GetBoolean(index); + item.IsVirtualItem = isVirtualItem; } - index++; - if (item is IHasSeries hasSeriesName) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var seriesName)) { - hasSeriesName.SeriesName = reader.GetString(index); + hasSeriesName.SeriesName = seriesName; } } @@ -1717,15 +1689,15 @@ namespace Emby.Server.Implementations.Data { if (item is Episode episode) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var seasonName)) { - episode.SeasonName = reader.GetString(index); + episode.SeasonName = seasonName; } index++; - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index, out var seasonId)) { - episode.SeasonId = reader.GetGuid(index); + episode.SeasonId = seasonId; } } else @@ -1741,9 +1713,9 @@ namespace Emby.Server.Implementations.Data { if (hasSeries != null) { - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index, out var seriesId)) { - hasSeries.SeriesId = reader.GetGuid(index); + hasSeries.SeriesId = seriesId; } } @@ -1752,56 +1724,48 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.PresentationUniqueKey)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var presentationUniqueKey)) { - item.PresentationUniqueKey = reader.GetString(index); + item.PresentationUniqueKey = presentationUniqueKey; } - - index++; } if (HasField(query, ItemFields.InheritedParentalRatingValue)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var parentalRating)) { - item.InheritedParentalRatingValue = reader.GetInt32(index); + item.InheritedParentalRatingValue = parentalRating; } - - index++; } if (HasField(query, ItemFields.ExternalSeriesId)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var externalSeriesId)) { - item.ExternalSeriesId = reader.GetString(index); + item.ExternalSeriesId = externalSeriesId; } - - index++; } if (HasField(query, ItemFields.Taglines)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var tagLine)) { - item.Tagline = reader.GetString(index); + item.Tagline = tagLine; } - - index++; } - if (!reader.IsDBNull(index)) + if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds)) { - DeserializeProviderIds(reader.GetString(index), item); + DeserializeProviderIds(providerIds, item); } index++; if (query.DtoOptions.EnableImages) { - if (!reader.IsDBNull(index)) + if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos)) { - DeserializeImages(reader.GetString(index), item); + item.ImageInfos = DeserializeImages(imageInfos); } index++; @@ -1809,72 +1773,62 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.ProductionLocations)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var productionLocations)) { - item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray(); + item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } if (HasField(query, ItemFields.ExtraIds)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var extraIds)) { - item.ExtraIds = SplitToGuids(reader.GetString(index)); + item.ExtraIds = SplitToGuids(extraIds); } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var totalBitrate)) { - item.TotalBitrate = reader.GetInt32(index); + item.TotalBitrate = totalBitrate; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var extraTypeString)) { - if (Enum.TryParse(reader.GetString(index), true, out ExtraType extraType)) + if (Enum.TryParse(extraTypeString, true, out ExtraType extraType)) { item.ExtraType = extraType; } } - index++; - if (hasArtistFields) { - if (item is IHasArtist hasArtists && !reader.IsDBNull(index)) + if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists)) { - hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; - if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index)) + if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists)) { - hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var externalId)) { - item.ExternalId = reader.GetString(index); + item.ExternalId = externalId; } - index++; - if (HasField(query, ItemFields.SeriesPresentationUniqueKey)) { if (hasSeries != null) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var seriesPresentationUniqueKey)) { - hasSeries.SeriesPresentationUniqueKey = reader.GetString(index); + hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; } } @@ -1883,21 +1837,19 @@ namespace Emby.Server.Implementations.Data if (enableProgramAttributes) { - if (item is LiveTvProgram program && !reader.IsDBNull(index)) + if (item is LiveTvProgram program && reader.TryGetString(index, out var showId)) { - program.ShowId = reader.GetString(index); + program.ShowId = showId; } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index, out var ownerId)) { - item.OwnerId = reader.GetGuid(index); + item.OwnerId = ownerId; } - index++; - return item; } @@ -1977,21 +1929,21 @@ namespace Emby.Server.Implementations.Data /// The reader. /// The item. /// ChapterInfo. - private ChapterInfo GetChapter(IReadOnlyList reader, BaseItem item) + private ChapterInfo GetChapter(IReadOnlyList reader, BaseItem item) { var chapter = new ChapterInfo { StartPositionTicks = reader.GetInt64(0) }; - if (!reader.IsDBNull(1)) + if (reader.TryGetString(1, out var chapterName)) { - chapter.Name = reader.GetString(1); + chapter.Name = chapterName; } - if (!reader.IsDBNull(2)) + if (reader.TryGetString(2, out var imagePath)) { - chapter.ImagePath = reader.GetString(2); + chapter.ImagePath = imagePath; if (!string.IsNullOrEmpty(chapter.ImagePath)) { @@ -2006,9 +1958,9 @@ namespace Emby.Server.Implementations.Data } } - if (!reader.IsDBNull(3)) + if (reader.TryReadDateTime(3, out var imageDateModified)) { - chapter.ImageDateModified = reader[3].ReadDateTime(); + chapter.ImageDateModified = imageDateModified; } return chapter; @@ -2116,30 +2068,7 @@ namespace Emby.Server.Implementations.Data || query.IsLiked.HasValue; } - private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) - .ToArray(); - - private string[] GetColumnNamesFromField(ItemFields field) - { - switch (field) - { - case ItemFields.Settings: - return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" }; - case ItemFields.ServiceName: - return new[] { "ExternalServiceId" }; - case ItemFields.SortName: - return new[] { "ForcedSortName" }; - case ItemFields.Taglines: - return new[] { "Tagline" }; - case ItemFields.Tags: - return new[] { "Tags" }; - case ItemFields.IsHD: - return Array.Empty(); - default: - return new[] { field.ToString() }; - } - } + private readonly ItemFields[] _allFields = Enum.GetValues(); private bool HasField(InternalItemsQuery query, ItemFields name) { @@ -2329,9 +2258,32 @@ namespace Emby.Server.Implementations.Data { if (!HasField(query, field)) { - foreach (var fieldToRemove in GetColumnNamesFromField(field)) + switch (field) { - list.Remove(fieldToRemove); + case ItemFields.Settings: + list.Remove("IsLocked"); + list.Remove("PreferredMetadataCountryCode"); + list.Remove("PreferredMetadataLanguage"); + list.Remove("LockedFields"); + break; + case ItemFields.ServiceName: + list.Remove("ExternalServiceId"); + break; + case ItemFields.SortName: + list.Remove("ForcedSortName"); + break; + case ItemFields.Taglines: + list.Remove("Tagline"); + break; + case ItemFields.Tags: + list.Remove("Tags"); + break; + case ItemFields.IsHD: + // do nothing + break; + default: + list.Remove(field.ToString()); + break; } } } @@ -2549,7 +2501,7 @@ namespace Emby.Server.Implementations.Data if (groups.Count > 0) { - return " Group by " + string.Join(",", groups); + return " Group by " + string.Join(',', groups); } return string.Empty; @@ -2578,9 +2530,9 @@ namespace Emby.Server.Implementations.Data } var commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query); + + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" })) + + GetFromText() + + GetJoinUserDataText(query); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) @@ -2630,7 +2582,7 @@ namespace Emby.Server.Implementations.Data } var commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns)) + + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns)) + GetFromText() + GetJoinUserDataText(query); @@ -2721,87 +2673,22 @@ namespace Emby.Server.Implementations.Data private string FixUnicodeChars(string buffer) { - if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2013', '-'); // en dash - } - - if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2014', '-'); // em dash - } - - if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2015', '-'); // horizontal bar - } - - if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2017', '_'); // double low line - } - - if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2018', '\''); // left single quotation mark - } - - if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2019', '\''); // right single quotation mark - } - - if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark - } - - if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark - } - - if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark - } - - if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark - } - - if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark - } - - if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis - } - - if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2032', '\''); // prime - } - - if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2033', '\"'); // double prime - } - - if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u0060', '\''); // grave accent - } - - if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u00B4', '\''); // acute accent - } - - return buffer; + buffer = buffer.Replace('\u2013', '-'); // en dash + buffer = buffer.Replace('\u2014', '-'); // em dash + buffer = buffer.Replace('\u2015', '-'); // horizontal bar + buffer = buffer.Replace('\u2017', '_'); // double low line + buffer = buffer.Replace('\u2018', '\''); // left single quotation mark + buffer = buffer.Replace('\u2019', '\''); // right single quotation mark + buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark + buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark + buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark + buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark + buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark + buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis + buffer = buffer.Replace('\u2032', '\''); // prime + buffer = buffer.Replace('\u2033', '\"'); // double prime + buffer = buffer.Replace('\u0060', '\''); // grave accent + return buffer.Replace('\u00B4', '\''); // acute accent } private void AddItem(List items, BaseItem newItem) @@ -2880,7 +2767,7 @@ namespace Emby.Server.Implementations.Data } var commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns)) + + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns)) + GetFromText() + GetJoinUserDataText(query); @@ -2923,15 +2810,15 @@ namespace Emby.Server.Implementations.Data if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); + commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); + commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); } else { - commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); + commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); } commandText += GetJoinUserDataText(query) @@ -3039,7 +2926,7 @@ namespace Emby.Server.Implementations.Data return string.Empty; } - return " ORDER BY " + string.Join(",", orderBy.Select(i => + return " ORDER BY " + string.Join(',', orderBy.Select(i => { var columnMap = MapOrderByField(i.Item1, query); @@ -3137,7 +3024,7 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; var commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" })) + + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" })) + GetFromText() + GetJoinUserDataText(query); @@ -3203,7 +3090,7 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText(); + var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText(); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) @@ -3245,12 +3132,8 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { var id = row.GetGuid(0); - string path = null; - if (!row.IsDBNull(1)) - { - path = row.GetString(1); - } + row.TryGetString(1, out var path); list.Add(new Tuple(id, path)); } @@ -3284,7 +3167,7 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; var commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" })) + + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" })) + GetFromText() + GetJoinUserDataText(query); @@ -3327,15 +3210,15 @@ namespace Emby.Server.Implementations.Data if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); + commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); + commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); } else { - commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); + commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); } commandText += GetJoinUserDataText(query) @@ -3584,11 +3467,11 @@ namespace Emby.Server.Implementations.Data statement?.TryBind("@IsFolder", query.IsFolder); } - var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray(); // Only specify excluded types if no included types are specified if (includeTypes.Length == 0) { - var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray(); if (excludeTypes.Length == 1) { whereClauses.Add("type<>@type"); @@ -3596,7 +3479,7 @@ namespace Emby.Server.Implementations.Data } else if (excludeTypes.Length > 1) { - var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'")); + var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'")); whereClauses.Add($"type not in ({inClause})"); } } @@ -3607,7 +3490,7 @@ namespace Emby.Server.Implementations.Data } else if (includeTypes.Length > 1) { - var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'")); + var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'")); whereClauses.Add($"type in ({inClause})"); } @@ -3618,7 +3501,7 @@ namespace Emby.Server.Implementations.Data } else if (query.ChannelIds.Count > 1) { - var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); whereClauses.Add($"ChannelId in ({inClause})"); } @@ -4351,7 +4234,7 @@ namespace Emby.Server.Implementations.Data } else if (query.Years.Length > 1) { - var val = string.Join(",", query.Years); + var val = string.Join(',', query.Years); whereClauses.Add("ProductionYear in (" + val + ")"); } @@ -4401,7 +4284,7 @@ namespace Emby.Server.Implementations.Data } else if (queryMediaTypes.Length > 1) { - var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'")); + var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'")); whereClauses.Add("MediaType in (" + val + ")"); } @@ -4444,7 +4327,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(string.Join(" AND ", excludeIds)); } - if (query.ExcludeProviderIds.Count > 0) + if (query.ExcludeProviderIds != null && query.ExcludeProviderIds.Count > 0) { var excludeIds = new List(); @@ -4474,7 +4357,7 @@ namespace Emby.Server.Implementations.Data } } - if (query.HasAnyProviderId.Count > 0) + if (query.HasAnyProviderId != null && query.HasAnyProviderId.Count > 0) { var hasProviderIds = new List(); @@ -4498,7 +4381,7 @@ namespace Emby.Server.Implementations.Data var paramName = "@HasAnyProviderId" + index; // this is a search for the placeholder - hasProviderIds.Add("ProviderIds like " + paramName + ""); + hasProviderIds.Add("ProviderIds like " + paramName); // this replaces the placeholder with a value, here: %key=val% if (statement != null) @@ -4532,7 +4415,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); } - var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); + var includedItemByNameTypes = GetItemByNameTypesInQuery(query); var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; var queryTopParentIds = query.TopParentIds; @@ -4549,7 +4432,7 @@ namespace Emby.Server.Implementations.Data } else if (enableItemsByName && includedItemByNameTypes.Count > 1) { - var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'")); + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); } else @@ -4564,7 +4447,7 @@ namespace Emby.Server.Implementations.Data } else if (queryTopParentIds.Length > 1) { - var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); if (enableItemsByName && includedItemByNameTypes.Count == 1) { @@ -4576,7 +4459,7 @@ namespace Emby.Server.Implementations.Data } else if (enableItemsByName && includedItemByNameTypes.Count > 1) { - var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'")); + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); } else @@ -4597,7 +4480,7 @@ namespace Emby.Server.Implementations.Data if (query.AncestorIds.Length > 1) { - var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); } @@ -4790,27 +4673,27 @@ namespace Emby.Server.Implementations.Data if (IsTypeInQuery(nameof(Person), query)) { - list.Add(nameof(Person)); + list.Add(typeof(Person).FullName); } if (IsTypeInQuery(nameof(Genre), query)) { - list.Add(nameof(Genre)); + list.Add(typeof(Genre).FullName); } if (IsTypeInQuery(nameof(MusicGenre), query)) { - list.Add(nameof(MusicGenre)); + list.Add(typeof(MusicGenre).FullName); } if (IsTypeInQuery(nameof(MusicArtist), query)) { - list.Add(nameof(MusicArtist)); + list.Add(typeof(MusicArtist).FullName); } if (IsTypeInQuery(nameof(Studio), query)) { - list.Add(nameof(Studio)); + list.Add(typeof(Studio).FullName); } return list; @@ -4915,15 +4798,10 @@ namespace Emby.Server.Implementations.Data typeof(AggregateFolder) }; - public void UpdateInheritedValues(CancellationToken cancellationToken) - { - UpdateInheritedTags(cancellationToken); - } - - private void UpdateInheritedTags(CancellationToken cancellationToken) + public void UpdateInheritedValues() { string sql = string.Join( - ";", + ';', new string[] { "delete from itemvalues where type = 6", @@ -4946,37 +4824,38 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - private static Dictionary GetTypeMapDictionary() + private static Dictionary GetTypeMapDictionary() { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var t in _knownTypes) { - dict[t.Name] = new[] { t.FullName }; + dict[t.Name] = t.FullName ; } - dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; - dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; + dict["Program"] = typeof(LiveTvProgram).FullName; + dict["TvChannel"] = typeof(LiveTvChannel).FullName; return dict; } // Not crazy about having this all the way down here, but at least it's in one place - private readonly Dictionary _types = GetTypeMapDictionary(); + private readonly Dictionary _types = GetTypeMapDictionary(); - private string[] MapIncludeItemTypes(string value) + private string MapIncludeItemTypes(string value) { - if (_types.TryGetValue(value, out string[] result)) + if (_types.TryGetValue(value, out string result)) { return result; } if (IsValidType(value)) { - return new[] { value }; + return value; } - return Array.Empty(); + Logger.LogWarning("Unknown item type: {ItemType}", value); + return null; } public void DeleteItem(Guid id) @@ -5148,7 +5027,7 @@ AND Type = @InternalPersonType)"); } else if (queryPersonTypes.Count > 1) { - var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'")); + var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'")); whereClauses.Add("PersonType in (" + val + ")"); } @@ -5162,7 +5041,7 @@ AND Type = @InternalPersonType)"); } else if (queryExcludePersonTypes.Count > 1) { - var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'")); + var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'")); whereClauses.Add("PersonType not in (" + val + ")"); } @@ -5279,48 +5158,63 @@ AND Type = @InternalPersonType)"); public List GetStudioNames() { - return GetItemValueNames(new[] { 3 }, new List(), new List()); + return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); } public List GetAllArtistNames() { - return GetItemValueNames(new[] { 0, 1 }, new List(), new List()); + return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); } public List GetMusicGenreNames() { - return GetItemValueNames(new[] { 2 }, new List { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }, new List()); + return GetItemValueNames( + new[] { 2 }, + new string[] + { + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }, + Array.Empty()); } public List GetGenreNames() { - return GetItemValueNames(new[] { 2 }, new List(), new List { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }); + return GetItemValueNames( + new[] { 2 }, + Array.Empty(), + new string[] + { + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }); } - private List GetItemValueNames(int[] itemValueTypes, List withItemTypes, List excludeItemTypes) + private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) { CheckDisposed(); - withItemTypes = withItemTypes.SelectMany(MapIncludeItemTypes).ToList(); - excludeItemTypes = excludeItemTypes.SelectMany(MapIncludeItemTypes).ToList(); - var now = DateTime.UtcNow; var typeClause = itemValueTypes.Length == 1 ? ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : - ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); + ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); var commandText = "Select Value From ItemValues where " + typeClause; if (withItemTypes.Count > 0) { - var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'")); + var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'")); commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))"; } if (excludeItemTypes.Count > 0) { - var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'")); + var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'")); commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))"; } @@ -5333,9 +5227,9 @@ AND Type = @InternalPersonType)"); { foreach (var row in statement.ExecuteQuery()) { - if (!row.IsDBNull(0)) + if (row.TryGetString(0, out var result)) { - list.Add(row.GetString(0)); + list.Add(result); } } } @@ -5363,7 +5257,7 @@ AND Type = @InternalPersonType)"); var typeClause = itemValueTypes.Length == 1 ? ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : - ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); + ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); InternalItemsQuery typeSubQuery = null; @@ -5415,7 +5309,6 @@ AND Type = @InternalPersonType)"); ItemIds = query.ItemIds, TopParentIds = query.TopParentIds, ParentId = query.ParentId, - IsPlayed = query.IsPlayed, IsAiring = query.IsAiring, IsMovie = query.IsMovie, IsSports = query.IsSports, @@ -5427,7 +5320,7 @@ AND Type = @InternalPersonType)"); columns = GetFinalColumnsToSelect(query, columns); var commandText = "select " - + string.Join(",", columns) + + string.Join(',', columns) + GetFromText() + GetJoinUserDataText(query); @@ -5441,6 +5334,7 @@ AND Type = @InternalPersonType)"); var outerQuery = new InternalItemsQuery(query.User) { + IsPlayed = query.IsPlayed, IsFavorite = query.IsFavorite, IsFavoriteOrLiked = query.IsFavoriteOrLiked, IsLiked = query.IsLiked, @@ -5469,7 +5363,9 @@ AND Type = @InternalPersonType)"); commandText += whereText + " group by PresentationUniqueKey"; - if (query.SimilarTo != null || !string.IsNullOrEmpty(query.SearchTerm)) + if (query.OrderBy.Count != 0 + || query.SimilarTo != null + || !string.IsNullOrEmpty(query.SearchTerm)) { commandText += GetOrderByText(query); } @@ -5504,7 +5400,7 @@ AND Type = @InternalPersonType)"); if (query.EnableTotalRecordCount) { var countText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText() + GetJoinUserDataText(query) + whereText; @@ -5565,7 +5461,7 @@ AND Type = @InternalPersonType)"); if (query.EnableTotalRecordCount) { commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText() + GetJoinUserDataText(query) + whereText; @@ -5607,7 +5503,7 @@ AND Type = @InternalPersonType)"); return result; } - private ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, string[] typesToCount) + private static ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, string[] typesToCount) { var counts = new ItemCounts(); @@ -5616,51 +5512,43 @@ AND Type = @InternalPersonType)"); return counts; } - var typeString = reader.IsDBNull(countStartColumn) ? null : reader.GetString(countStartColumn); - - if (string.IsNullOrWhiteSpace(typeString)) + if (!reader.TryGetString(countStartColumn, out var typeString)) { return counts; } - var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries) - .ToLookup(x => x); - - foreach (var type in allTypes) + foreach (var typeName in typeString.AsSpan().Split('|')) { - var value = type.Count(); - var typeName = type.Key; - - if (string.Equals(typeName, typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) + if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.SeriesCount = value; + counts.SeriesCount++; } - else if (string.Equals(typeName, typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.EpisodeCount = value; + counts.EpisodeCount++; } - else if (string.Equals(typeName, typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.MovieCount = value; + counts.MovieCount++; } - else if (string.Equals(typeName, typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.AlbumCount = value; + counts.AlbumCount++; } - else if (string.Equals(typeName, typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.ArtistCount = value; + counts.ArtistCount++; } - else if (string.Equals(typeName, typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.SongCount = value; + counts.SongCount++; } - else if (string.Equals(typeName, typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.TrailerCount = value; + counts.TrailerCount++; } - counts.ItemCount += value; + counts.ItemCount++; } return counts; @@ -5809,7 +5697,10 @@ AND Type = @InternalPersonType)"); var endIndex = Math.Min(people.Count, startIndex + Limit); for (var i = startIndex; i < endIndex; i++) { - insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", i.ToString(CultureInfo.InvariantCulture)); + insertText.AppendFormat( + CultureInfo.InvariantCulture, + "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", + i.ToString(CultureInfo.InvariantCulture)); } // Remove last comma @@ -5843,7 +5734,7 @@ AND Type = @InternalPersonType)"); } } - private PersonInfo GetPerson(IReadOnlyList reader) + private PersonInfo GetPerson(IReadOnlyList reader) { var item = new PersonInfo { @@ -5851,19 +5742,19 @@ AND Type = @InternalPersonType)"); Name = reader.GetString(1) }; - if (!reader.IsDBNull(2)) + if (reader.TryGetString(2, out var role)) { - item.Role = reader.GetString(2); + item.Role = role; } - if (!reader.IsDBNull(3)) + if (reader.TryGetString(3, out var type)) { - item.Type = reader.GetString(3); + item.Type = type; } - if (!reader.IsDBNull(4)) + if (reader.TryGetInt32(4, out var sortOrder)) { - item.SortOrder = reader.GetInt32(4); + item.SortOrder = sortOrder; } return item; @@ -6050,7 +5941,7 @@ AND Type = @InternalPersonType)"); /// /// The reader. /// ChapterInfo. - private MediaStream GetMediaStream(IReadOnlyList reader) + private MediaStream GetMediaStream(IReadOnlyList reader) { var item = new MediaStream { @@ -6059,157 +5950,157 @@ AND Type = @InternalPersonType)"); item.Type = Enum.Parse(reader[2].ToString(), true); - if (reader[3].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(3, out var codec)) { - item.Codec = reader[3].ToString(); + item.Codec = codec; } - if (reader[4].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(4, out var language)) { - item.Language = reader[4].ToString(); + item.Language = language; } - if (reader[5].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(5, out var channelLayout)) { - item.ChannelLayout = reader[5].ToString(); + item.ChannelLayout = channelLayout; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(6, out var profile)) { - item.Profile = reader[6].ToString(); + item.Profile = profile; } - if (reader[7].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(7, out var aspectRatio)) { - item.AspectRatio = reader[7].ToString(); + item.AspectRatio = aspectRatio; } - if (reader[8].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(8, out var path)) { - item.Path = RestorePath(reader[8].ToString()); + item.Path = RestorePath(path); } item.IsInterlaced = reader.GetBoolean(9); - if (reader[10].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(10, out var bitrate)) { - item.BitRate = reader.GetInt32(10); + item.BitRate = bitrate; } - if (reader[11].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(11, out var channels)) { - item.Channels = reader.GetInt32(11); + item.Channels = channels; } - if (reader[12].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(12, out var sampleRate)) { - item.SampleRate = reader.GetInt32(12); + item.SampleRate = sampleRate; } item.IsDefault = reader.GetBoolean(13); item.IsForced = reader.GetBoolean(14); item.IsExternal = reader.GetBoolean(15); - if (reader[16].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(16, out var width)) { - item.Width = reader.GetInt32(16); + item.Width = width; } - if (reader[17].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(17, out var height)) { - item.Height = reader.GetInt32(17); + item.Height = height; } - if (reader[18].SQLiteType != SQLiteType.Null) + if (reader.TryGetSingle(18, out var averageFrameRate)) { - item.AverageFrameRate = reader.GetFloat(18); + item.AverageFrameRate = averageFrameRate; } - if (reader[19].SQLiteType != SQLiteType.Null) + if (reader.TryGetSingle(19, out var realFrameRate)) { - item.RealFrameRate = reader.GetFloat(19); + item.RealFrameRate = realFrameRate; } - if (reader[20].SQLiteType != SQLiteType.Null) + if (reader.TryGetSingle(20, out var level)) { - item.Level = reader.GetFloat(20); + item.Level = level; } - if (reader[21].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(21, out var pixelFormat)) { - item.PixelFormat = reader[21].ToString(); + item.PixelFormat = pixelFormat; } - if (reader[22].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(22, out var bitDepth)) { - item.BitDepth = reader.GetInt32(22); + item.BitDepth = bitDepth; } - if (reader[23].SQLiteType != SQLiteType.Null) + if (reader.TryGetBoolean(23, out var isAnamorphic)) { - item.IsAnamorphic = reader.GetBoolean(23); + item.IsAnamorphic = isAnamorphic; } - if (reader[24].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(24, out var refFrames)) { - item.RefFrames = reader.GetInt32(24); + item.RefFrames = refFrames; } - if (reader[25].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(25, out var codecTag)) { - item.CodecTag = reader.GetString(25); + item.CodecTag = codecTag; } - if (reader[26].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(26, out var comment)) { - item.Comment = reader.GetString(26); + item.Comment = comment; } - if (reader[27].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(27, out var nalLengthSize)) { - item.NalLengthSize = reader.GetString(27); + item.NalLengthSize = nalLengthSize; } - if (reader[28].SQLiteType != SQLiteType.Null) + if (reader.TryGetBoolean(28, out var isAVC)) { - item.IsAVC = reader[28].ToBool(); + item.IsAVC = isAVC; } - if (reader[29].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(29, out var title)) { - item.Title = reader[29].ToString(); + item.Title = title; } - if (reader[30].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(30, out var timeBase)) { - item.TimeBase = reader[30].ToString(); + item.TimeBase = timeBase; } - if (reader[31].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(31, out var codecTimeBase)) { - item.CodecTimeBase = reader[31].ToString(); + item.CodecTimeBase = codecTimeBase; } - if (reader[32].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(32, out var colorPrimaries)) { - item.ColorPrimaries = reader[32].ToString(); + item.ColorPrimaries = colorPrimaries; } - if (reader[33].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(33, out var colorSpace)) { - item.ColorSpace = reader[33].ToString(); + item.ColorSpace = colorSpace; } - if (reader[34].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(34, out var colorTransfer)) { - item.ColorTransfer = reader[34].ToString(); + item.ColorTransfer = colorTransfer; } if (item.Type == MediaStreamType.Subtitle) { - item.localizedUndefined = _localization.GetLocalizedString("Undefined"); - item.localizedDefault = _localization.GetLocalizedString("Default"); - item.localizedForced = _localization.GetLocalizedString("Forced"); + item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); + item.LocalizedDefault = _localization.GetLocalizedString("Default"); + item.LocalizedForced = _localization.GetLocalizedString("Forced"); } return item; @@ -6261,7 +6152,7 @@ AND Type = @InternalPersonType)"); CheckDisposed(); if (id == Guid.Empty) { - throw new ArgumentException(nameof(id)); + throw new ArgumentException("Guid can't be empty.", nameof(id)); } if (attachments == null) @@ -6351,36 +6242,36 @@ AND Type = @InternalPersonType)"); /// /// The reader. /// MediaAttachment. - private MediaAttachment GetMediaAttachment(IReadOnlyList reader) + private MediaAttachment GetMediaAttachment(IReadOnlyList reader) { var item = new MediaAttachment { Index = reader[1].ToInt() }; - if (reader[2].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(2, out var codec)) { - item.Codec = reader[2].ToString(); + item.Codec = codec; } - if (reader[2].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(3, out var codecTag)) { - item.CodecTag = reader[3].ToString(); + item.CodecTag = codecTag; } - if (reader[4].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(4, out var comment)) { - item.Comment = reader[4].ToString(); + item.Comment = comment; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(5, out var fileName)) { - item.FileName = reader[5].ToString(); + item.FileName = fileName; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(6, out var mimeType)) { - item.MimeType = reader[6].ToString(); + item.MimeType = mimeType; } return item; diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 2c4e8e0fcc..ef9af1dcd0 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction( db => { - db.ExecuteAll(string.Join(";", new[] { + db.ExecuteAll(string.Join(';', new[] { "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", @@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data /// Read a row from the specified reader into the provided userData object. /// /// - private UserItemData ReadRow(IReadOnlyList reader) + private UserItemData ReadRow(IReadOnlyList reader) { var userData = new UserItemData(); userData.Key = reader[0].ToString(); // userData.UserId = reader[1].ReadGuidFromBlob(); - if (reader[2].SQLiteType != SQLiteType.Null) + if (reader.TryGetDouble(2, out var rating)) { - userData.Rating = reader[2].ToDouble(); + userData.Rating = rating; } userData.Played = reader[3].ToBool(); @@ -365,19 +367,19 @@ namespace Emby.Server.Implementations.Data userData.IsFavorite = reader[5].ToBool(); userData.PlaybackPositionTicks = reader[6].ToInt64(); - if (reader[7].SQLiteType != SQLiteType.Null) + if (reader.TryReadDateTime(7, out var lastPlayedDate)) { - userData.LastPlayedDate = reader[7].TryReadDateTime(); + userData.LastPlayedDate = lastPlayedDate; } - if (reader[8].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(8, out var audioStreamIndex)) { - userData.AudioStreamIndex = reader[8].ToInt(); + userData.AudioStreamIndex = audioStreamIndex; } - if (reader[9].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(9, out var subtitleStreamIndex)) { - userData.SubtitleStreamIndex = reader[9].ToInt(); + userData.SubtitleStreamIndex = subtitleStreamIndex; } return userData; diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs index 7044b1d194..7f1306d15a 100644 --- a/Emby.Server.Implementations/Data/TypeMapper.cs +++ b/Emby.Server.Implementations/Data/TypeMapper.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data /// This holds all the types in the running assemblies /// so that we can de-serialize properly when we don't have strong types. /// - private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); /// /// Gets the type. @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Data /// Name of the type. /// Type. /// typeName is null. - public Type GetType(string typeName) + public Type? GetType(string typeName) { if (string.IsNullOrEmpty(typeName)) { @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Data /// /// Name of the type. /// Type. - private Type LookupType(string typeName) + private Type? LookupType(string typeName) { return AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(typeName)) diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs index fa6ac95fd3..3d15b3e768 100644 --- a/Emby.Server.Implementations/Devices/DeviceId.cs +++ b/Emby.Server.Implementations/Devices/DeviceId.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index da5047d244..2637addce7 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 686944a286..7411239a1e 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -249,7 +251,7 @@ namespace Emby.Server.Implementations.Dto var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); if (activeRecording != null) { - dto.Type = "Recording"; + dto.Type = BaseItemKind.Recording; dto.CanDownload = false; dto.RunTimeTicks = null; @@ -582,7 +584,26 @@ namespace Emby.Server.Implementations.Dto { baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture); - baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes; + if (dto.ImageBlurHashes != null) + { + // Only add BlurHash for the person's image. + baseItemPerson.ImageBlurHashes = new Dictionary>(); + foreach (var (imageType, blurHash) in dto.ImageBlurHashes) + { + if (blurHash != null) + { + baseItemPerson.ImageBlurHashes[imageType] = new Dictionary(); + foreach (var (imageId, blurHashValue) in blurHash) + { + if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase)) + { + baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue; + } + } + } + } + } + list.Add(baseItemPerson); } } @@ -646,10 +667,7 @@ namespace Emby.Server.Implementations.Dto var tag = GetImageCacheTag(item, image); if (!string.IsNullOrEmpty(image.BlurHash)) { - if (dto.ImageBlurHashes == null) - { - dto.ImageBlurHashes = new Dictionary>(); - } + dto.ImageBlurHashes ??= new Dictionary>(); if (!dto.ImageBlurHashes.ContainsKey(image.Type)) { @@ -683,10 +701,7 @@ namespace Emby.Server.Implementations.Dto if (hashes.Count > 0) { - if (dto.ImageBlurHashes == null) - { - dto.ImageBlurHashes = new Dictionary>(); - } + dto.ImageBlurHashes ??= new Dictionary>(); dto.ImageBlurHashes[imageType] = hashes; } @@ -879,13 +894,10 @@ namespace Emby.Server.Implementations.Dto dto.Taglines = new string[] { item.Tagline }; } - if (dto.Taglines == null) - { - dto.Taglines = Array.Empty(); - } + dto.Taglines ??= Array.Empty(); } - dto.Type = item.GetClientTypeName(); + dto.Type = item.GetBaseItemKind(); if ((item.CommunityRating ?? 0) > 0) { dto.CommunityRating = item.CommunityRating; @@ -1138,7 +1150,7 @@ namespace Emby.Server.Implementations.Dto if (episodeSeries != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); - if (!dto.ImageTags.ContainsKey(ImageType.Primary)) + if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary)) { AttachPrimaryImageAspectRatio(dto, episodeSeries); } @@ -1188,7 +1200,7 @@ namespace Emby.Server.Implementations.Dto if (series != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); - if (!dto.ImageTags.ContainsKey(ImageType.Primary)) + if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary)) { AttachPrimaryImageAspectRatio(dto, series); } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 9e9452f32c..57e040338b 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,6 +9,7 @@ + @@ -23,24 +24,16 @@ - - - - - - - - + - - - - - + + + + @@ -52,27 +45,24 @@ false true true + enable AD0001 + AllEnabledByDefault + ../jellyfin.ruleset - - - ../jellyfin.ruleset - - - diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 14201ead29..0a4efd73c7 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints NatUtility.StartDiscovery(); _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); - - _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; } private void Stop() @@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints NatUtility.DeviceFound -= OnNatUtilityDeviceFound; _timer?.Dispose(); - - _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; - } - - private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs e) - { - NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp); } private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index ae1b51b4c3..5bb4100ba9 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 824bb85f44..e0ca02d986 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 9486874d58..2e72b18f57 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,3 +1,4 @@ +using System; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.EntryPoints /// /// The UDP server. /// - private UdpServer _udpServer; + private UdpServer? _udpServer; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private bool _disposed = false; @@ -49,10 +50,12 @@ namespace Emby.Server.Implementations.EntryPoints /// public Task RunAsync() { + CheckDisposed(); + try { - _udpServer = new UdpServer(_logger, _appHost, _config); - _udpServer.Start(PortNumber, _cancellationTokenSource.Token); + _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber); + _udpServer.Start(_cancellationTokenSource.Token); } catch (SocketException ex) { @@ -62,6 +65,14 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + } + /// public void Dispose() { @@ -71,9 +82,8 @@ namespace Emby.Server.Implementations.EntryPoints } _cancellationTokenSource.Cancel(); - _udpServer.Dispose(); _cancellationTokenSource.Dispose(); - _cancellationTokenSource = null; + _udpServer?.Dispose(); _udpServer = null; _disposed = true; diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 1989e9ed25..d3bcd5e13e 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly Dictionary> _changedItems = new Dictionary>(); private readonly object _syncLock = new object(); - private Timer _updateTimer; + private Timer? _updateTimer; public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager) { @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e) + private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e) { if (e.SaveReason == UserDataSaveReason.PlaybackProgress) { @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints _updateTimer.Change(UpdateDuration, Timeout.Infinite); } - if (!_changedItems.TryGetValue(e.UserId, out List keys)) + if (!_changedItems.TryGetValue(e.UserId, out List? keys)) { keys = new List(); _changedItems[e.UserId] = keys; @@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints } } - private void UpdateTimerCallback(object state) + private void UpdateTimerCallback(object? state) { lock (_syncLock) { diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 4a0fc8239e..9afabf5272 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 -using System; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index ab43e088b9..95be6552b0 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) { - return (AuthorizationInfo)cached; + return (AuthorizationInfo)cached!; // Cache should never contain null } return GetAuthorization(requestContext); @@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security } private AuthorizationInfo GetAuthorizationInfoFromDictionary( - in Dictionary auth, + in Dictionary? auth, in IHeaderDictionary headers, in IQueryCollection queryString) { - string deviceId = null; - string device = null; - string client = null; - string version = null; - string token = null; + string? deviceId = null; + string? device = null; + string? client = null; + string? version = null; + string? token = null; if (auth != null) { @@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorizationDictionary(HttpContext httpReq) + private Dictionary? GetAuthorizationDictionary(HttpContext httpReq) { var auth = httpReq.Request.Headers["X-Emby-Authorization"]; @@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security auth = httpReq.Request.Headers[HeaderNames.Authorization]; } - return GetAuthorization(auth); + return GetAuthorization(auth.Count > 0 ? auth[0] : null); } /// @@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorizationDictionary(HttpRequest httpReq) + private Dictionary? GetAuthorizationDictionary(HttpRequest httpReq) { var auth = httpReq.Headers["X-Emby-Authorization"]; @@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security auth = httpReq.Headers[HeaderNames.Authorization]; } - return GetAuthorization(auth); + return GetAuthorization(auth.Count > 0 ? auth[0] : null); } /// @@ -240,25 +240,25 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The authorization header. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorization(string authorizationHeader) + private Dictionary? GetAuthorization(ReadOnlySpan authorizationHeader) { if (authorizationHeader == null) { return null; } - var parts = authorizationHeader.Split(' ', 2); + var firstSpace = authorizationHeader.IndexOf(' '); - // There should be at least to parts - if (parts.Length != 2) + // There should be at least two parts + if (firstSpace == -1) { return null; } - var acceptedNames = new[] { "MediaBrowser", "Emby" }; + var name = authorizationHeader[..firstSpace]; - // It has to be a digest request - if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase)) + if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase) + && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase)) { return null; } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 86914dea20..c375f36ce4 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user); } public SessionInfo GetSession(object requestContext) @@ -36,16 +36,16 @@ namespace Emby.Server.Implementations.HttpServer.Security return GetSession((HttpContext)requestContext); } - public User GetUser(HttpContext requestContext) + public User? GetUser(HttpContext requestContext) { var session = GetSession(requestContext); return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); } - public User GetUser(object requestContext) + public User? GetUser(object requestContext) { - return GetUser((HttpContext)requestContext); + return GetUser(((HttpRequest)requestContext).HttpContext); } } } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index fed2addf80..8f7d606692 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -1,10 +1,9 @@ -#nullable enable - using System; using System.Buffers; using System.IO.Pipelines; using System.Net; using System.Net.WebSockets; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer RemoteEndPoint = remoteEndPoint; QueryString = query; - _jsonOptions = JsonDefaults.GetOptions(); + _jsonOptions = JsonDefaults.Options; LastActivityDate = DateTime.Now; } @@ -138,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer writer.Advance(bytesRead); // Make the data available to the PipeReader - FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false); + FlushResult flushResult = await writer.FlushAsync(cancellationToken).ConfigureAwait(false); if (flushResult.IsCompleted) { // The PipeReader stopped reading @@ -181,32 +180,16 @@ namespace Emby.Server.Implementations.HttpServer } WebSocketMessage? stub; + long bytesConsumed = 0; try { - - if (buffer.IsSingleSegment) - { - stub = JsonSerializer.Deserialize>(buffer.FirstSpan, _jsonOptions); - } - else - { - var buf = ArrayPool.Shared.Rent(Convert.ToInt32(buffer.Length)); - try - { - buffer.CopyTo(buf); - stub = JsonSerializer.Deserialize>(buf, _jsonOptions); - } - finally - { - ArrayPool.Shared.Return(buf); - } - } + stub = DeserializeWebSocketMessage(buffer, out bytesConsumed); } catch (JsonException ex) { // Tell the PipeReader how much of the buffer we have consumed reader.AdvanceTo(buffer.End); - _logger.LogError(ex, "Error processing web socket message"); + _logger.LogError(ex, "Error processing web socket message: {Data}", Encoding.UTF8.GetString(buffer)); return; } @@ -217,27 +200,34 @@ namespace Emby.Server.Implementations.HttpServer } // Tell the PipeReader how much of the buffer we have consumed - reader.AdvanceTo(buffer.End); + reader.AdvanceTo(buffer.GetPosition(bytesConsumed)); _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub); - var info = new WebSocketMessageInfo - { - MessageType = stub.MessageType, - Data = stub.Data?.ToString(), // Data can be null - Connection = this - }; - - if (info.MessageType == SessionMessageType.KeepAlive) + if (stub.MessageType == SessionMessageType.KeepAlive) { await SendKeepAliveResponse().ConfigureAwait(false); } else { - await OnReceive(info).ConfigureAwait(false); + await OnReceive( + new WebSocketMessageInfo + { + MessageType = stub.MessageType, + Data = stub.Data?.ToString(), // Data can be null + Connection = this + }).ConfigureAwait(false); } } + internal WebSocketMessage? DeserializeWebSocketMessage(ReadOnlySequence bytes, out long bytesConsumed) + { + var jsonReader = new Utf8JsonReader(bytes); + var ret = JsonSerializer.Deserialize>(ref jsonReader, _jsonOptions); + bytesConsumed = jsonReader.BytesConsumed; + return ret; + } + private Task SendKeepAliveResponse() { LastKeepAliveDate = DateTime.UtcNow; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index d6cf6233e4..861c0a95e3 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -14,15 +16,18 @@ namespace Emby.Server.Implementations.HttpServer public class WebSocketManager : IWebSocketManager { private readonly IWebSocketListener[] _webSocketListeners; + private readonly IAuthService _authService; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; public WebSocketManager( + IAuthService authService, IEnumerable webSocketListeners, ILogger logger, ILoggerFactory loggerFactory) { _webSocketListeners = webSocketListeners.ToArray(); + _authService = authService; _logger = logger; _loggerFactory = loggerFactory; } @@ -30,6 +35,7 @@ namespace Emby.Server.Implementations.HttpServer /// public async Task WebSocketRequestHandler(HttpContext context) { + _ = _authService.Authenticate(context.Request); try { _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 7435e9d0bf..47a83d77ce 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 3353fae9d8..aa80bccd72 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 3cb025111d..6a554e68ab 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -2,11 +2,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Text; +using System.Runtime.InteropServices; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; @@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.IO private readonly List _shortcutHandlers = new List(); private readonly string _tempPath; - private readonly bool _isEnvironmentCaseInsensitive; + private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public ManagedFileSystem( ILogger logger, @@ -32,8 +31,6 @@ namespace Emby.Server.Implementations.IO { Logger = logger; _tempPath = applicationPaths.TempDirectory; - - _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; } public virtual void AddShortcutHandler(IShortcutHandler handler) @@ -64,7 +61,7 @@ namespace Emby.Server.Implementations.IO /// The filename. /// System.String. /// filename - public virtual string ResolveShortcut(string filename) + public virtual string? ResolveShortcut(string filename) { if (string.IsNullOrEmpty(filename)) { @@ -72,7 +69,7 @@ namespace Emby.Server.Implementations.IO } var extension = Path.GetExtension(filename); - var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); return handler?.Resolve(filename); } @@ -249,13 +246,20 @@ namespace Emby.Server.Implementations.IO // Issue #2354 get the size of files behind symbolic links if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) { - using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) + try { - result.Length = thisFileStream.Length; + using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) + { + result.Length = thisFileStream.Length; + } + } + catch (FileNotFoundException ex) + { + // Dangling symlinks cannot be detected before opening the file unfortunately... + Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName); + result.Exists = false; } } - - result.DirectoryName = fileInfo.DirectoryName; } result.CreationTimeUtc = GetCreationTimeUtc(info); @@ -294,16 +298,37 @@ namespace Emby.Server.Implementations.IO /// The filename. /// System.String. /// The filename is null. - public virtual string GetValidFilename(string filename) + public string GetValidFilename(string filename) { - var builder = new StringBuilder(filename); - - foreach (var c in Path.GetInvalidFileNameChars()) + var invalid = Path.GetInvalidFileNameChars(); + var first = filename.IndexOfAny(invalid); + if (first == -1) { - builder = builder.Replace(c, ' '); + // Fast path for clean strings + return filename; } - return builder.ToString(); + return string.Create( + filename.Length, + (filename, invalid, first), + (chars, state) => + { + state.filename.AsSpan().CopyTo(chars); + + chars[state.first++] = ' '; + + var len = chars.Length; + foreach (var c in state.invalid) + { + for (int i = state.first; i < len; i++) + { + if (chars[i] == c) + { + chars[i] = ' '; + } + } + } + }); } /// @@ -487,26 +512,9 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(path)); } - var separatorChar = Path.DirectorySeparatorChar; - - return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1; - } - - public virtual bool IsRootPath(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - var parent = Path.GetDirectoryName(path); - - if (!string.IsNullOrEmpty(parent)) - { - return false; - } - - return true; + return path.Contains( + Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar, + _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } public virtual string NormalizePath(string path) @@ -521,7 +529,7 @@ namespace Emby.Server.Implementations.IO return path; } - return path.TrimEnd(Path.DirectorySeparatorChar); + return Path.TrimEndingDirectorySeparator(path); } public virtual bool AreEqual(string path1, string path2) @@ -536,7 +544,10 @@ namespace Emby.Server.Implementations.IO return false; } - return string.Equals(NormalizePath(path1), NormalizePath(path2), StringComparison.OrdinalIgnoreCase); + return string.Equals( + NormalizePath(path1), + NormalizePath(path2), + _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } public virtual string GetFileNameWithoutExtension(FileSystemMetadata info) @@ -582,9 +593,7 @@ namespace Emby.Server.Implementations.IO public virtual IEnumerable GetDirectories(string path, bool recursive = false) { - var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - - return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption)); + return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive))); } public virtual IEnumerable GetFiles(string path, bool recursive = false) @@ -592,18 +601,18 @@ namespace Emby.Server.Implementations.IO return GetFiles(path, null, false, recursive); } - public virtual IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { - var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var enumerationOptions = GetEnumerationOptions(recursive); // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1) { - return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption)); + return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions)); } - var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption); + var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions); if (extensions != null && extensions.Count > 0) { @@ -625,10 +634,10 @@ namespace Emby.Server.Implementations.IO public virtual IEnumerable GetFileSystemEntries(string path, bool recursive = false) { var directoryInfo = new DirectoryInfo(path); - var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var enumerationOptions = GetEnumerationOptions(recursive); - return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption)) - .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption))); + return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions)) + .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions))); } private IEnumerable ToMetadata(IEnumerable infos) @@ -638,8 +647,7 @@ namespace Emby.Server.Implementations.IO public virtual IEnumerable GetDirectoryPaths(string path, bool recursive = false) { - var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - return Directory.EnumerateDirectories(path, "*", searchOption); + return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive)); } public virtual IEnumerable GetFilePaths(string path, bool recursive = false) @@ -647,18 +655,18 @@ namespace Emby.Server.Implementations.IO return GetFilePaths(path, null, false, recursive); } - public virtual IEnumerable GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { - var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var enumerationOptions = GetEnumerationOptions(recursive); // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) { - return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption); + return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions); } - var files = Directory.EnumerateFiles(path, "*", searchOption); + var files = Directory.EnumerateFiles(path, "*", enumerationOptions); if (extensions != null && extensions.Length > 0) { @@ -679,23 +687,18 @@ namespace Emby.Server.Implementations.IO public virtual IEnumerable GetFileSystemEntryPaths(string path, bool recursive = false) { - var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - return Directory.EnumerateFileSystemEntries(path, "*", searchOption); + return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive)); } - private static void RunProcess(string path, string args, string workingDirectory) + private EnumerationOptions GetEnumerationOptions(bool recursive) { - using (var process = Process.Start(new ProcessStartInfo - { - Arguments = args, - FileName = path, - CreateNoWindow = true, - WorkingDirectory = workingDirectory, - WindowStyle = ProcessWindowStyle.Normal - })) + return new EnumerationOptions { - process.WaitForExit(); - } + RecurseSubdirectories = recursive, + IgnoreInaccessible = true, + // Don't skip any files. + AttributesToSkip = 0 + }; } } } diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs index e6696b8c4c..76c58d5dcd 100644 --- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs +++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO public string Extension => ".mblink"; - public string Resolve(string shortcutPath) + public string? Resolve(string shortcutPath) { if (string.IsNullOrEmpty(shortcutPath)) { diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index c16ebd61b7..e4f5f4cf0b 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) + public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(bufferSize); try diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 4bef59543f..a430b9e720 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -1,7 +1,5 @@ #pragma warning disable CS1591 -using System; - namespace Emby.Server.Implementations { public interface IStartupOptions @@ -9,7 +7,7 @@ namespace Emby.Server.Implementations /// /// Gets the value of the --ffmpeg command line option. /// - string FFmpegPath { get; } + string? FFmpegPath { get; } /// /// Gets the value of the --service command line option. @@ -19,21 +17,21 @@ namespace Emby.Server.Implementations /// /// Gets the value of the --package-name command line option. /// - string PackageName { get; } + string? PackageName { get; } /// /// Gets the value of the --restartpath command line option. /// - string RestartPath { get; } + string? RestartPath { get; } /// /// Gets the value of the --restartargs command line option. /// - string RestartArgs { get; } + string? RestartArgs { get; } /// /// Gets the value of the --published-server-url command line option. /// - Uri PublishedServerUrl { get; } + string? PublishedServerUrl { get; } } } diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs index afa4ec7b1b..e96b64595c 100644 --- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs +++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs @@ -2,20 +2,12 @@ using System; using System.Collections.Generic; -using System.Linq; -using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Images { diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 5f7e51858a..833fb0b7a1 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray() }; - if (options.InputPaths.Length == 0) + if (options.InputPaths.Count == 0) { return null; } diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 161b4c4528..ff5f26ce09 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 462eb03a80..900b3fd9c6 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -1,10 +1,11 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs index 0224ab32a0..859017f869 100644 --- a/Emby.Server.Implementations/Images/FolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 3817882312..6da431c68e 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs index 0ce1b91e88..b8f0f0d65c 100644 --- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; @@ -29,9 +31,7 @@ namespace Emby.Server.Implementations.Images { var subItem = i.Item2; - var episode = subItem as Episode; - - if (episode != null) + if (subItem is Episode episode) { var series = episode.Series; if (series != null && series.HasImage(ImageType.Primary)) diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs index 236453e805..6c65b58999 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index e30a675931..5384c04b3b 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Linq; using DotNet.Globbing; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index db27862ce7..f8d8197d46 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -48,6 +50,7 @@ using MediaBrowser.Providers.MediaInfo; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using Genre = MediaBrowser.Controller.Entities.Genre; using Person = MediaBrowser.Controller.Entities.Person; using VideoResolver = Emby.Naming.Video.VideoResolver; @@ -175,10 +178,7 @@ namespace Emby.Server.Implementations.Library { lock (_rootFolderSyncLock) { - if (_rootFolder == null) - { - _rootFolder = CreateRootFolder(); - } + _rootFolder ??= CreateRootFolder(); } } @@ -196,33 +196,33 @@ namespace Emby.Server.Implementations.Library /// Gets or sets the postscan tasks. /// /// The postscan tasks. - private ILibraryPostScanTask[] PostscanTasks { get; set; } + private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty(); /// /// Gets or sets the intro providers. /// /// The intro providers. - private IIntroProvider[] IntroProviders { get; set; } + private IIntroProvider[] IntroProviders { get; set; } = Array.Empty(); /// /// Gets or sets the list of entity resolution ignore rules. /// /// The entity resolution ignore rules. - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty(); /// /// Gets or sets the list of currently registered entity resolvers. /// /// The entity resolvers enumerable. - private IItemResolver[] EntityResolvers { get; set; } + private IItemResolver[] EntityResolvers { get; set; } = Array.Empty(); - private IMultiItemResolver[] MultiItemResolvers { get; set; } + private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty(); /// /// Gets or sets the comparers. /// /// The comparers. - private IBaseItemComparer[] Comparers { get; set; } + private IBaseItemComparer[] Comparers { get; set; } = Array.Empty(); public bool IsScanRunning { get; private set; } @@ -558,7 +558,6 @@ namespace Emby.Server.Implementations.Library var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) { Parent = parent, - Path = fullPath, FileInfo = fileInfo, CollectionType = collectionType, LibraryOptions = libraryOptions @@ -684,7 +683,7 @@ namespace Emby.Server.Implementations.Library foreach (var item in items) { - ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService); + ResolverHelper.SetInitialItemValues(item, parent, this, directoryService); } items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions)); @@ -1163,7 +1162,7 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } - _itemRepository.UpdateInheritedValues(cancellationToken); + _itemRepository.UpdateInheritedValues(); progress.Report(100); } @@ -1240,11 +1239,20 @@ namespace Emby.Server.Implementations.Library return info; } - private string GetCollectionType(string path) + private CollectionTypeOptions? GetCollectionType(string path) { - return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false) - .Select(Path.GetFileNameWithoutExtension) - .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false); + foreach (var file in files) + { + // TODO: @bond use a ReadOnlySpan here when Enum.TryParse supports it + // https://github.com/dotnet/runtime/issues/20008 + if (Enum.TryParse(Path.GetFileNameWithoutExtension(file), true, out var res)) + { + return res; + } + } + + return null; } /// @@ -1905,12 +1913,17 @@ namespace Emby.Server.Implementations.Library } catch (ArgumentException) { - _logger.LogWarning("Cannot get image index for {0}", img.Path); + _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path); + continue; + } + catch (Exception ex) when (ex is InvalidOperationException || ex is IOException) + { + _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path); continue; } - catch (InvalidOperationException) + catch (HttpRequestException ex) { - _logger.LogWarning("Cannot fetch image from {0}", img.Path); + _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode); continue; } } @@ -1923,7 +1936,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path); + _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); image.Width = 0; image.Height = 0; continue; @@ -1935,7 +1948,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path); + _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path); image.BlurHash = string.Empty; } @@ -1945,7 +1958,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path); + _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path); } } @@ -2503,7 +2516,7 @@ namespace Emby.Server.Implementations.Library public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { var series = episode.Series; - bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); + bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); if (!isAbsoluteNaming.Value) { // In other words, no filter applied @@ -2515,9 +2528,23 @@ 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? - var episodeInfo = episode.IsFileProtocol - ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path) - : new Naming.TV.EpisodeInfo(episode.Path); + EpisodeInfo episodeInfo = null; + if (episode.IsFileProtocol) + { + episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming); + // Resolve from parent folder if it's not the Season folder + if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder)) + { + episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming); + if (episodeInfo != null) + { + // add the container + episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.'); + } + } + } + + episodeInfo ??= new EpisodeInfo(episode.Path); try { @@ -2767,6 +2794,7 @@ namespace Emby.Server.Implementations.Library public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) { + string newPath; if (ownerItem != null) { var libraryOptions = GetLibraryOptions(ownerItem); @@ -2774,15 +2802,9 @@ namespace Emby.Server.Implementations.Library { foreach (var pathInfo in libraryOptions.PathInfos) { - if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath)) - { - continue; - } - - var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath); - if (substitutionResult.Item2) + if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath)) { - return substitutionResult.Item1; + return newPath; } } } @@ -2791,24 +2813,16 @@ namespace Emby.Server.Implementations.Library var metadataPath = _configurationManager.Configuration.MetadataPath; var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; - if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath)) + if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath)) { - var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath); - if (metadataSubstitutionResult.Item2) - { - return metadataSubstitutionResult.Item1; - } + return newPath; } foreach (var map in _configurationManager.Configuration.PathSubstitutions) { - if (!string.IsNullOrWhiteSpace(map.From)) + if (path.TryReplaceSubPath(map.From, map.To, out newPath)) { - var substitutionResult = SubstitutePathInternal(path, map.From, map.To); - if (substitutionResult.Item2) - { - return substitutionResult.Item1; - } + return newPath; } } @@ -2817,47 +2831,12 @@ namespace Emby.Server.Implementations.Library public string SubstitutePath(string path, string from, string to) { - return SubstitutePathInternal(path, from, to).Item1; - } - - private Tuple SubstitutePathInternal(string path, string from, string to) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - if (string.IsNullOrWhiteSpace(from)) - { - throw new ArgumentNullException(nameof(from)); - } - - if (string.IsNullOrWhiteSpace(to)) - { - throw new ArgumentNullException(nameof(to)); - } - - from = from.Trim(); - to = to.Trim(); - - var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase); - var changed = false; - - if (!string.Equals(newPath, path, StringComparison.Ordinal)) + if (path.TryReplaceSubPath(from, to, out var newPath)) { - if (to.IndexOf('/', StringComparison.Ordinal) != -1) - { - newPath = newPath.Replace('\\', '/'); - } - else - { - newPath = newPath.Replace('/', '\\'); - } - - changed = true; + return newPath; } - return new Tuple(newPath, changed); + return path; } private void SetExtraTypeFromFilename(Video item) @@ -2914,6 +2893,12 @@ namespace Emby.Server.Implementations.Library } public void UpdatePeople(BaseItem item, List people) + { + UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + public async Task UpdatePeopleAsync(BaseItem item, List people, CancellationToken cancellationToken) { if (!item.SupportsPeople) { @@ -2921,6 +2906,8 @@ namespace Emby.Server.Implementations.Library } _itemRepository.UpdatePeople(item.Id, people); + + await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); } public async Task ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) @@ -2956,7 +2943,7 @@ namespace Emby.Server.Implementations.Library throw new InvalidOperationException(); } - public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) + public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) { @@ -2990,9 +2977,9 @@ namespace Emby.Server.Implementations.Library { Directory.CreateDirectory(virtualFolderPath); - if (!string.IsNullOrEmpty(collectionType)) + if (collectionType != null) { - var path = Path.Combine(virtualFolderPath, collectionType + ".collection"); + var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); File.WriteAllBytes(path, Array.Empty()); } @@ -3024,6 +3011,58 @@ namespace Emby.Server.Implementations.Library } } + private async Task SavePeopleMetadataAsync(IEnumerable people, CancellationToken cancellationToken) + { + var personsToSave = new List(); + + foreach (var person in people) + { + cancellationToken.ThrowIfCancellationRequested(); + + var itemUpdateType = ItemUpdateType.MetadataDownload; + var saveEntity = false; + var personEntity = GetPerson(person.Name); + + // if PresentationUniqueKey is empty it's likely a new item. + if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + { + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); + saveEntity = true; + } + + foreach (var id in person.ProviderIds) + { + if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)) + { + personEntity.SetProviderId(id.Key, id.Value); + saveEntity = true; + } + } + + if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary)) + { + personEntity.SetImage( + new ItemImageInfo + { + Path = person.ImageUrl, + Type = ImageType.Primary + }, + 0); + + saveEntity = true; + itemUpdateType = ItemUpdateType.ImageUpdate; + } + + if (saveEntity) + { + personsToSave.Add(personEntity); + await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + } + } + + CreateItems(personsToSave, null, CancellationToken.None); + } + private void StartScanInBackground() { Task.Run(() => diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 041619d1e5..4ef7923db3 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -5,16 +7,17 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library @@ -23,14 +26,13 @@ namespace Emby.Server.Implementations.Library { private readonly IMediaEncoder _mediaEncoder; private readonly ILogger _logger; - private readonly IJsonSerializer _json; private readonly IApplicationPaths _appPaths; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths) + public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths) { _mediaEncoder = mediaEncoder; _logger = logger; - _json = json; _appPaths = appPaths; } @@ -47,7 +49,8 @@ namespace Emby.Server.Implementations.Library { try { - mediaInfo = _json.DeserializeFromFile(cacheFilePath); + await using FileStream jsonStream = File.OpenRead(cacheFilePath); + mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); } @@ -83,7 +86,8 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath != null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - _json.SerializeToFile(mediaInfo, cacheFilePath); + await using FileStream createStream = File.OpenWrite(cacheFilePath); + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 928f5f88e4..38e81d14c4 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -6,12 +8,14 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -23,7 +27,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library @@ -36,7 +39,6 @@ namespace Emby.Server.Implementations.Library private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; - private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; @@ -46,6 +48,7 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private IMediaSourceProvider[] _providers; @@ -56,7 +59,6 @@ namespace Emby.Server.Implementations.Library IUserManager userManager, ILibraryManager libraryManager, ILogger logger, - IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, IMediaEncoder mediaEncoder) @@ -65,7 +67,6 @@ namespace Emby.Server.Implementations.Library _userManager = userManager; _libraryManager = libraryManager; _logger = logger; - _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; _userDataManager = userDataManager; _mediaEncoder = mediaEncoder; @@ -200,10 +201,15 @@ namespace Emby.Server.Implementations.Library { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } + else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); + source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); + } } } - return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList(); + return SortMediaSources(list); } public MediaProtocol GetPathProtocol(string path) @@ -437,7 +443,7 @@ namespace Emby.Server.Implementations.Library } } - private static IEnumerable SortMediaSources(IEnumerable sources) + private static List SortMediaSources(IEnumerable sources) { return sources.OrderBy(i => { @@ -452,8 +458,9 @@ namespace Emby.Server.Implementations.Library { var stream = i.VideoStream; - return stream == null || stream.Width == null ? 0 : stream.Width.Value; + return stream?.Width ?? 0; }) + .Where(i => i.Type != MediaSourceType.Placeholder) .ToList(); } @@ -504,7 +511,7 @@ namespace Emby.Server.Implementations.Library // hack - these two values were taken from LiveTVMediaSourceProvider string cacheKey = request.OpenToken; - await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths) + await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) .ConfigureAwait(false); } @@ -516,9 +523,9 @@ namespace Emby.Server.Implementations.Library } // TODO: @bond Fix - var json = _jsonSerializer.SerializeToString(mediaSource); + var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions); _logger.LogInformation("Live stream opened: " + json); - var clone = _jsonSerializer.DeserializeFromString(json); + var clone = JsonSerializer.Deserialize(json, _jsonOptions); if (!request.UserId.Equals(Guid.Empty)) { @@ -585,18 +592,9 @@ namespace Emby.Server.Implementations.Library public Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) { - var info = _openStreams.Values.FirstOrDefault(i => - { - var liveStream = i as ILiveStream; - if (liveStream != null) - { - return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); - } - - return false; - }); + var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase)); - return Task.FromResult(info as IDirectStreamProvider); + return Task.FromResult(info.Value as IDirectStreamProvider); } public async Task OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) @@ -643,7 +641,8 @@ namespace Emby.Server.Implementations.Library { try { - mediaInfo = _jsonSerializer.DeserializeFromFile(cacheFilePath); + await using FileStream jsonStream = File.OpenRead(cacheFilePath); + mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); } @@ -679,7 +678,8 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath != null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath); + await using FileStream createStream = File.Create(cacheFilePath); + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 28fa062396..b833122ea0 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 658c53f288..06300adebc 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -100,8 +102,7 @@ namespace Emby.Server.Implementations.Library public List GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions) { - var genre = item as MusicGenre; - if (genre != null) + if (item is MusicGenre genre) { return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 06ff3e611b..86b8039fab 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,7 +1,6 @@ -#nullable enable - using System; -using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; +using MediaBrowser.Common.Providers; namespace Emby.Server.Implementations.Library { @@ -41,11 +40,78 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); - return m.Success ? m.Value : null; + var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId); + return match ? imdbId.ToString() : null; } return null; } + + /// + /// Replaces a sub path with another sub path and normalizes the final path. + /// + /// The original path. + /// The original sub path. + /// The new sub path. + /// The result of the sub path replacement + /// The path after replacing the sub path. + /// , or is empty. + public static bool TryReplaceSubPath( + [NotNullWhen(true)] this string? path, + [NotNullWhen(true)] string? subPath, + [NotNullWhen(true)] string? newSubPath, + [NotNullWhen(true)] out string? newPath) + { + newPath = null; + + if (string.IsNullOrEmpty(path) + || string.IsNullOrEmpty(subPath) + || string.IsNullOrEmpty(newSubPath) + || subPath.Length > path.Length) + { + return false; + } + + char oldDirectorySeparatorChar; + char newDirectorySeparatorChar; + // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162 + // The reasoning behind this is that a forward slash likely means it's a Linux path and + // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much). + if (newSubPath.Contains('/', StringComparison.Ordinal)) + { + oldDirectorySeparatorChar = '\\'; + newDirectorySeparatorChar = '/'; + } + else + { + oldDirectorySeparatorChar = '/'; + newDirectorySeparatorChar = '\\'; + } + + path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar); + subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar); + + // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results + // when the sub path matches a similar but in-complete subpath + var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar; + if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (path.Length > subPath.Length + && !oldSubPathEndsWithSeparator + && path[subPath.Length] != newDirectorySeparatorChar) + { + return false; + } + + var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar); + // Ensure that the path with the old subpath removed starts with a leading dir separator + int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length; + newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx)); + + return true; + } } } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 4e4cac75bf..ac75e5d3a1 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -18,11 +18,10 @@ namespace Emby.Server.Implementations.Library /// /// The item. /// The parent. - /// The file system. /// The library manager. /// The directory service. - /// Item must have a path - public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService) + /// Item must have a path. + public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService) { // This version of the below method has no ItemResolveArgs, so we have to require the path already being set if (string.IsNullOrEmpty(item.Path)) @@ -43,9 +42,14 @@ namespace Emby.Server.Implementations.Library // Make sure DateCreated and DateModified have values var fileInfo = directoryService.GetFile(item.Path); - SetDateCreated(item, fileSystem, fileInfo); + if (fileInfo == null) + { + throw new FileNotFoundException("Can't find item path.", item.Path); + } - EnsureName(item, item.Path, fileInfo); + SetDateCreated(item, fileInfo); + + EnsureName(item, fileInfo); } /// @@ -72,9 +76,9 @@ namespace Emby.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); // Make sure the item has a name - EnsureName(item, item.Path, args.FileInfo); + EnsureName(item, args.FileInfo); - 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 @@ -84,28 +88,15 @@ namespace Emby.Server.Implementations.Library /// /// Ensures the name. /// - private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo) + private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo) { // If the subclass didn't supply a name, add it here - if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath)) + if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path)) { - var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name; - - item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory); + item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name); } } - /// - /// Gets the display name. - /// - /// The path. - /// if set to true [is directory]. - /// System.String. - private static string GetDisplayName(string path, bool isDirectory) - { - return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); - } - /// /// Ensures DateCreated and DateModified have values. /// @@ -114,21 +105,6 @@ namespace Emby.Server.Implementations.Library /// The args. private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args) { - if (fileSystem == null) - { - throw new ArgumentNullException(nameof(fileSystem)); - } - - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - // See if a different path came out of the resolver than what went in if (!fileSystem.AreEqual(args.Path, item.Path)) { @@ -136,7 +112,7 @@ namespace Emby.Server.Implementations.Library if (childData != null) { - SetDateCreated(item, fileSystem, childData); + SetDateCreated(item, childData); } else { @@ -144,17 +120,17 @@ namespace Emby.Server.Implementations.Library if (fileData.Exists) { - SetDateCreated(item, fileSystem, fileData); + SetDateCreated(item, fileData); } } } else { - SetDateCreated(item, fileSystem, args.FileInfo); + SetDateCreated(item, args.FileInfo); } } - private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info) + private static void SetDateCreated(BaseItem item, FileSystemMetadata? info) { var config = BaseItem.ConfigurationManager.GetMetadataConfiguration(); @@ -163,7 +139,7 @@ namespace Emby.Server.Implementations.Library // directoryService.getFile may return null if (info != null) { - var dateCreated = fileSystem.GetCreationTimeUtc(info); + var dateCreated = info.CreationTimeUtc; if (dateCreated.Equals(DateTime.MinValue)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 2c4497c693..e893d63350 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// Gets the priority. /// /// The priority. - public override ResolverPriority Priority => ResolverPriority.Fourth; + public override ResolverPriority Priority => ResolverPriority.Fifth; public MultiItemResolverResult ResolveMultiple( Folder parent, @@ -201,6 +203,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio continue; } + if (resolvedItem.Files.Count == 0) + { + continue; + } + var firstMedia = resolvedItem.Files[0]; var libraryItem = new T diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 18ceb5e761..8e1eccb10a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -40,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// Gets the priority. /// /// The priority. - public override ResolverPriority Priority => ResolverPriority.Second; + public override ResolverPriority Priority => ResolverPriority.Third; /// /// Resolves the specified args. diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index e9e688fa67..3d2ae95d24 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Linq; using System.Threading.Tasks; @@ -79,11 +81,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return new MusicArtist(); } - if (_config.Configuration.EnableSimpleArtistDetection) - { - return null; - } - // Avoid mis-identifying top folders if (args.Parent.IsRoot) { diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 2f5e46038d..a3dcdc9441 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// /// The args. /// `0. - protected override T Resolve(ItemResolveArgs args) + public override T Resolve(ItemResolveArgs args) { return ResolveVideo(args, false); } @@ -42,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// The args. /// if set to true [parse name]. /// ``0. - protected TVideoType ResolveVideo(ItemResolveArgs args, bool parseName) + protected virtual TVideoType ResolveVideo(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); @@ -165,13 +167,13 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void SetVideoType(Video video, VideoFileInfo videoInfo) { - var extension = Path.GetExtension(video.Path); - video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) || - string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ? - VideoType.Iso : - VideoType.VideoFile; + var extension = Path.GetExtension(video.Path.AsSpan()); + video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".img", StringComparison.OrdinalIgnoreCase) + ? VideoType.Iso + : VideoType.VideoFile; - video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); video.IsPlaceHolder = videoInfo.IsStub; if (videoInfo.IsStub) @@ -193,11 +195,11 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (video.VideoType == VideoType.Iso) { - if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1) + if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase)) { video.IsoType = IsoType.Dvd; } - else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1) + else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase)) { video.IsoType = IsoType.BluRay; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 59af7ce8ac..68076730b3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -11,9 +13,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver { - private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" }; - protected override Book Resolve(ItemResolveArgs args) + public override Book Resolve(ItemResolveArgs args) { var collectionType = args.GetCollectionType(); diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs index 7dbce7a6ef..7aaee017dd 100644 --- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index 9ca76095b2..fa45ccf840 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -11,6 +13,12 @@ namespace Emby.Server.Implementations.Library.Resolvers public abstract class ItemResolver : IItemResolver where T : BaseItem, new() { + /// + /// Gets the priority. + /// + /// The priority. + public virtual ResolverPriority Priority => ResolverPriority.First; + /// /// Resolves the specified args. /// @@ -21,12 +29,6 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - /// - /// Gets the priority. - /// - /// The priority. - public virtual ResolverPriority Priority => ResolverPriority.First; - /// /// Sets initial values on the newly resolved item. /// diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index 295e9e120b..69d71d0d9a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.IO; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index baf0e3cf91..02c5287646 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.IO; @@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// Gets the priority. /// /// The priority. - public override ResolverPriority Priority => ResolverPriority.Third; + public override ResolverPriority Priority => ResolverPriority.Fourth; /// public MultiItemResolverResult ResolveMultiple( @@ -69,6 +71,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } + /// + /// Resolves the specified args. + /// + /// The args. + /// Video. + public override Video Resolve(ItemResolveArgs args) + { + var collectionType = args.GetCollectionType(); + + // Find movies with their own folders + if (args.IsDirectory) + { + if (IsInvalid(args.Parent, collectionType)) + { + return null; + } + + var files = args.FileSystemChildren + .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) + .ToList(); + + if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + { + return FindMovie(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + } + + if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) + { + return FindMovie