Merge branch 'master' into segment-deletion

This commit is contained in:
Dominik 2023-06-15 19:38:42 +02:00 committed by GitHub
commit 17f1e8d19b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1007 changed files with 41859 additions and 38419 deletions

View File

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
default: 6.0.x
default: 7.0.x
jobs:
- job: CompatibilityCheck

View File

@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 6.0.x
DotNetSdkVersion: 7.0.x
jobs:
- job: Build
@ -20,35 +20,6 @@ jobs:
submodules: true
persistCredentials: true
- task: DownloadPipelineArtifact@2
displayName: 'Download Web Branch'
condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')
inputs:
path: '$(Agent.TempDirectory)'
artifact: 'jellyfin-web-production'
source: 'specific'
project: 'jellyfin'
pipeline: 'Jellyfin Web'
runBranch: variables['Build.SourceBranch']
- task: DownloadPipelineArtifact@2
displayName: 'Download Web Target'
condition: eq(variables['Build.Reason'], 'PullRequest')
inputs:
path: '$(Agent.TempDirectory)'
artifact: 'jellyfin-web-production'
source: 'specific'
project: 'jellyfin'
pipeline: 'Jellyfin Web'
runBranch: variables['System.PullRequest.TargetBranch']
- task: ExtractFiles@1
displayName: 'Extract Web Client'
inputs:
archiveFilePatterns: '$(Agent.TempDirectory)/*.zip'
destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard'
cleanDestinationFolder: false
- task: UseDotNet@2
displayName: 'Update DotNet'
inputs:

View File

@ -32,8 +32,10 @@ jobs:
BuildConfiguration: linux.armhf
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
BuildConfiguration: macos
MacOS.amd64:
BuildConfiguration: macos.amd64
MacOS.arm64:
BuildConfiguration: macos.arm64
Portable:
BuildConfiguration: portable
@ -205,10 +207,10 @@ jobs:
steps:
- task: UseDotNet@2
displayName: 'Use .NET 6.0 sdk'
displayName: 'Use .NET 7.0 sdk'
inputs:
packageType: 'sdk'
version: '6.0.x'
version: '7.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'

View File

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 6.0.x
default: 7.0.x
jobs:
- job: Test
@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json"
artifactName: 'OpenAPI Spec'

View File

@ -30,9 +30,9 @@ body:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- 10.8.0
- 10.8.z
- 10.8.9
- 10.7.7
- 10.7.z
- 10.6.4
- Other
validations:
@ -47,13 +47,15 @@ body:
label: Environment
description: |
Examples:
- **OS**: [e.g. Debian, Windows]
- **OS**: [e.g. Debian 11, Windows 10]
- **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
- **Virtualization**: [e.g. Docker, KVM, LXC]
- **Clients**: [Browser, Android, Fire Stick, etc.]
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
@ -61,12 +63,14 @@ body:
- **Storage**: [e.g. local, NFS, cloud]
value: |
- OS:
- Linux Kernel:
- Virtualization:
- Clients:
- Browser:
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
- GPU Model:
- Plugins:
- Reverse Proxy:
- Base URL:
@ -84,8 +88,8 @@ body:
id: ffmpeg-logs
attributes:
label: FFmpeg logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
render: shell
- type: textarea
id: browserlogs

View File

@ -7,6 +7,7 @@ on:
pull_request_target:
issue_comment:
permissions: {}
jobs:
label:
name: Labeling
@ -18,6 +19,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:
@ -26,7 +28,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@ -35,7 +37,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@ -44,7 +46,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@ -58,7 +60,7 @@ jobs:
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@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@ -67,7 +69,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:

View File

@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- name: Setup .NET Core
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '6.0.x'
dotnet-version: '7.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2
uses: github/codeql-action/init@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2
uses: github/codeql-action/autobuild@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2
uses: github/codeql-action/analyze@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6

View File

@ -9,6 +9,7 @@ on:
- labeled
- synchronize
permissions: {}
jobs:
rebase:
name: Rebase
@ -16,30 +17,33 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
check-backport:
permissions:
contents: read
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@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@ -47,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@ -89,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@ -104,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -5,6 +5,8 @@ on:
- master
pull_request_target:
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
@ -12,23 +14,23 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '6.0.x'
dotnet-version: '7.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json
openapi-base:
name: OpenAPI - BASE
@ -37,24 +39,35 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
ref: ${{ github.base_ref }}
- name: Setup .NET Core
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Checkout common ancestor
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '6.0.x'
dotnet-version: '7.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json
openapi-diff:
permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
@ -63,12 +76,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-base
path: openapi-base
@ -90,14 +103,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
uses: peter-evans/find-comment@b657a70ff16d17651703a84bee1cb9ad9d2be2ea # tag=v2
uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@ -112,7 +125,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}

View File

@ -1,27 +1,51 @@
name: Issue Stale Check
name: Stale Check
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
issues:
name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
operations-per-run: 75
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
prs-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.

4
.gitignore vendored
View File

@ -150,8 +150,6 @@ publish/
*.pubxml
# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
# packages/
dlls/
dllssigned/
@ -166,7 +164,6 @@ AppPackages/
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
@ -276,7 +273,6 @@ BenchmarkDotNet.Artifacts
# Ignore web artifacts from native builds
web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated
# Omnisharp crash logs

3
.npmrc
View File

@ -1,3 +0,0 @@
registry=https://registry.npmjs.org/
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
always-auth=true

10
.vscode/launch.json vendored
View File

@ -2,11 +2,11 @@
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"name": ".NET Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net7.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@ -18,11 +18,11 @@
}
},
{
"name": ".NET Core Launch (nowebclient)",
"name": ".NET Launch (nowebclient)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net7.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@ -30,7 +30,7 @@
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": ".NET Core Attach",
"name": ".NET Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"

View File

@ -27,6 +27,7 @@
- [cvium](https://github.com/cvium)
- [dannymichel](https://github.com/dannymichel)
- [DaveChild](https://github.com/DaveChild)
- [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan)
- [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung)
@ -37,6 +38,7 @@
- [DMouse10462](https://github.com/DMouse10462)
- [DrPandemic](https://github.com/DrPandemic)
- [eglia](https://github.com/eglia)
- [EgorBakanov](https://github.com/EgorBakanov)
- [EraYaN](https://github.com/EraYaN)
- [escabe](https://github.com/escabe)
- [excelite](https://github.com/excelite)
@ -56,6 +58,7 @@
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
- [JustAMan](https://github.com/JustAMan)
@ -123,6 +126,7 @@
- [SuperSandro2000](https://github.com/SuperSandro2000)
- [tbraeutigam](https://github.com/tbraeutigam)
- [teacupx](https://github.com/teacupx)
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
- [Terror-Gene](https://github.com/Terror-Gene)
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
@ -160,6 +164,8 @@
- [vgambier](https://github.com/vgambier)
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [RealGreenDragon](https://github.com/RealGreenDragon)
- [ipitio](https://github.com/ipitio)
- [TheTyrius](https://github.com/TheTyrius)
# Emby Contributors
@ -229,3 +235,4 @@
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)

View File

@ -15,7 +15,8 @@
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="$(SolutionDir)/BannedSymbols.txt" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/BannedSymbols.txt" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
</ItemGroup>
</Project>

90
Directory.Packages.props Normal file
View File

@ -0,0 +1,90 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
<PackageVersion Include="AutoFixture" Version="4.18.0" />
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.5" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.0.2" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageVersion Include="System.Text.Json" Version="7.0.2" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.4.2" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist
FROM debian:stable-slim as app
@ -37,7 +38,7 @@ RUN apt-get update \
&& apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers \
jellyfin-ffmpeg \
jellyfin-ffmpeg5 \
openssl \
locales \
# Intel VAAPI Tone mapping dependencies:

View File

@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder
@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu

View File

@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder
@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu

View File

@ -1,25 +0,0 @@
#pragma warning disable CS1591
using System.Buffers.Binary;
using System.IO;
namespace DvdLib
{
public class BigEndianBinaryReader : BinaryReader
{
public BigEndianBinaryReader(Stream input)
: base(input)
{
}
public override ushort ReadUInt16()
{
return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
}
public override uint ReadUInt32()
{
return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4));
}
}
}

View File

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@ -1,23 +0,0 @@
#pragma warning disable CS1591
using System.IO;
namespace DvdLib.Ifo
{
public class Cell
{
public CellPlaybackInfo PlaybackInfo { get; private set; }
public CellPositionInfo PositionInfo { get; private set; }
internal void ParsePlayback(BinaryReader br)
{
PlaybackInfo = new CellPlaybackInfo(br);
}
internal void ParsePosition(BinaryReader br)
{
PositionInfo = new CellPositionInfo(br);
}
}
}

View File

@ -1,52 +0,0 @@
#pragma warning disable CS1591
using System.IO;
namespace DvdLib.Ifo
{
public enum BlockMode
{
NotInBlock = 0,
FirstCell = 1,
InBlock = 2,
LastCell = 3,
}
public enum BlockType
{
Normal = 0,
Angle = 1,
}
public enum PlaybackMode
{
Normal = 0,
StillAfterEachVOBU = 1,
}
public class CellPlaybackInfo
{
public readonly BlockMode Mode;
public readonly BlockType Type;
public readonly bool SeamlessPlay;
public readonly bool Interleaved;
public readonly bool STCDiscontinuity;
public readonly bool SeamlessAngle;
public readonly PlaybackMode PlaybackMode;
public readonly bool Restricted;
public readonly byte StillTime;
public readonly byte CommandNumber;
public readonly DvdTime PlaybackTime;
public readonly uint FirstSector;
public readonly uint FirstILVUEndSector;
public readonly uint LastVOBUStartSector;
public readonly uint LastSector;
internal CellPlaybackInfo(BinaryReader br)
{
br.BaseStream.Seek(0x4, SeekOrigin.Current);
PlaybackTime = new DvdTime(br.ReadBytes(4));
br.BaseStream.Seek(0x10, SeekOrigin.Current);
}
}
}

View File

@ -1,19 +0,0 @@
#pragma warning disable CS1591
using System.IO;
namespace DvdLib.Ifo
{
public class CellPositionInfo
{
public readonly ushort VOBId;
public readonly byte CellId;
internal CellPositionInfo(BinaryReader br)
{
VOBId = br.ReadUInt16();
br.ReadByte();
CellId = br.ReadByte();
}
}
}

View File

@ -1,20 +0,0 @@
#pragma warning disable CS1591
namespace DvdLib.Ifo
{
public class Chapter
{
public ushort ProgramChainNumber { get; private set; }
public ushort ProgramNumber { get; private set; }
public uint ChapterNumber { get; private set; }
public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
{
ProgramChainNumber = pgcNum;
ProgramNumber = programNum;
ChapterNumber = chapterNum;
}
}
}

View File

@ -1,167 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
namespace DvdLib.Ifo
{
public class Dvd
{
private readonly ushort _titleSetCount;
public readonly List<Title> Titles;
private ushort _titleCount;
public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
public Dvd(string path)
{
Titles = new List<Title>();
var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
if (vmgPath == null)
{
foreach (var ifo in allFiles)
{
if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{
ReadVTS(ifoNumber, ifo.FullName);
}
}
}
else
{
using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var vmgRead = new BigEndianBinaryReader(vmgFs))
{
vmgFs.Seek(0x3E, SeekOrigin.Begin);
_titleSetCount = vmgRead.ReadUInt16();
// read address of TT_SRPT
vmgFs.Seek(0xC4, SeekOrigin.Begin);
uint ttSectorPtr = vmgRead.ReadUInt32();
vmgFs.Seek(ttSectorPtr * 2048, SeekOrigin.Begin);
ReadTT_SRPT(vmgRead);
}
}
for (ushort titleSetNum = 1; titleSetNum <= _titleSetCount; titleSetNum++)
{
ReadVTS(titleSetNum, allFiles);
}
}
}
private void ReadTT_SRPT(BinaryReader read)
{
_titleCount = read.ReadUInt16();
read.BaseStream.Seek(6, SeekOrigin.Current);
for (uint titleNum = 1; titleNum <= _titleCount; titleNum++)
{
var t = new Title(titleNum);
t.ParseTT_SRPT(read);
Titles.Add(t);
}
}
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
{
var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
if (vtsPath == null)
{
throw new FileNotFoundException("Unable to find VTS IFO file");
}
ReadVTS(vtsNum, vtsPath.FullName);
}
private void ReadVTS(ushort vtsNum, string vtsPath)
{
VTSPaths[vtsNum] = vtsPath;
using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var vtsRead = new BigEndianBinaryReader(vtsFs))
{
// Read VTS_PTT_SRPT
vtsFs.Seek(0xC8, SeekOrigin.Begin);
uint vtsPttSrptSecPtr = vtsRead.ReadUInt32();
uint baseAddr = (vtsPttSrptSecPtr * 2048);
vtsFs.Seek(baseAddr, SeekOrigin.Begin);
ushort numTitles = vtsRead.ReadUInt16();
vtsRead.ReadUInt16();
uint endaddr = vtsRead.ReadUInt32();
uint[] offsets = new uint[numTitles];
for (ushort titleNum = 0; titleNum < numTitles; titleNum++)
{
offsets[titleNum] = vtsRead.ReadUInt32();
}
for (uint titleNum = 0; titleNum < numTitles; titleNum++)
{
uint chapNum = 1;
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
if (t == null)
{
continue;
}
do
{
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
{
break;
}
chapNum++;
}
while (vtsFs.Position < (baseAddr + endaddr));
}
// Read VTS_PGCI
vtsFs.Seek(0xCC, SeekOrigin.Begin);
uint vtsPgciSecPtr = vtsRead.ReadUInt32();
vtsFs.Seek(vtsPgciSecPtr * 2048, SeekOrigin.Begin);
long startByte = vtsFs.Position;
ushort numPgcs = vtsRead.ReadUInt16();
vtsFs.Seek(6, SeekOrigin.Current);
for (ushort pgcNum = 1; pgcNum <= numPgcs; pgcNum++)
{
byte pgcCat = vtsRead.ReadByte();
bool entryPgc = (pgcCat & 0x80) != 0;
uint titleNum = (uint)(pgcCat & 0x7F);
vtsFs.Seek(3, SeekOrigin.Current);
uint vtsPgcOffset = vtsRead.ReadUInt32();
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
if (t != null)
{
t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
}
}
}
}
}
}
}

View File

@ -1,39 +0,0 @@
#pragma warning disable CS1591
using System;
namespace DvdLib.Ifo
{
public class DvdTime
{
public readonly byte Hour, Minute, Second, Frames, FrameRate;
public DvdTime(byte[] data)
{
Hour = GetBCDValue(data[0]);
Minute = GetBCDValue(data[1]);
Second = GetBCDValue(data[2]);
Frames = GetBCDValue((byte)(data[3] & 0x3F));
if ((data[3] & 0x80) != 0)
{
FrameRate = 30;
}
else if ((data[3] & 0x40) != 0)
{
FrameRate = 25;
}
}
private static byte GetBCDValue(byte data)
{
return (byte)((((data & 0xF0) >> 4) * 10) + (data & 0x0F));
}
public static explicit operator TimeSpan(DvdTime time)
{
int ms = (int)(((1.0 / (double)time.FrameRate) * time.Frames) * 1000.0);
return new TimeSpan(0, time.Hour, time.Minute, time.Second, ms);
}
}
}

View File

@ -1,16 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace DvdLib.Ifo
{
public class Program
{
public IReadOnlyList<Cell> Cells { get; }
public Program(List<Cell> cells)
{
Cells = cells;
}
}
}

View File

@ -1,121 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace DvdLib.Ifo
{
public enum ProgramPlaybackMode
{
Sequential,
Random,
Shuffle
}
public class ProgramChain
{
private byte _programCount;
public readonly List<Program> Programs;
private byte _cellCount;
public readonly List<Cell> Cells;
public DvdTime PlaybackTime { get; private set; }
public UserOperation ProhibitedUserOperations { get; private set; }
public byte[] AudioStreamControl { get; private set; } // 8*2 entries
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
private ushort _nextProgramNumber;
private ushort _prevProgramNumber;
private ushort _goupProgramNumber;
public ProgramPlaybackMode PlaybackMode { get; private set; }
public uint ProgramCount { get; private set; }
public byte StillTime { get; private set; }
public byte[] Palette { get; private set; } // 16*4 entries
private ushort _commandTableOffset;
private ushort _programMapOffset;
private ushort _cellPlaybackOffset;
private ushort _cellPositionOffset;
public readonly uint VideoTitleSetIndex;
internal ProgramChain(uint vtsPgcNum)
{
VideoTitleSetIndex = vtsPgcNum;
Cells = new List<Cell>();
Programs = new List<Program>();
}
internal void ParseHeader(BinaryReader br)
{
long startPos = br.BaseStream.Position;
br.ReadUInt16();
_programCount = br.ReadByte();
_cellCount = br.ReadByte();
PlaybackTime = new DvdTime(br.ReadBytes(4));
ProhibitedUserOperations = (UserOperation)br.ReadUInt32();
AudioStreamControl = br.ReadBytes(16);
SubpictureStreamControl = br.ReadBytes(128);
_nextProgramNumber = br.ReadUInt16();
_prevProgramNumber = br.ReadUInt16();
_goupProgramNumber = br.ReadUInt16();
StillTime = br.ReadByte();
byte pbMode = br.ReadByte();
if (pbMode == 0)
{
PlaybackMode = ProgramPlaybackMode.Sequential;
}
else
{
PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
}
ProgramCount = (uint)(pbMode & 0x7F);
Palette = br.ReadBytes(64);
_commandTableOffset = br.ReadUInt16();
_programMapOffset = br.ReadUInt16();
_cellPlaybackOffset = br.ReadUInt16();
_cellPositionOffset = br.ReadUInt16();
// read position info
br.BaseStream.Seek(startPos + _cellPositionOffset, SeekOrigin.Begin);
for (int cellNum = 0; cellNum < _cellCount; cellNum++)
{
var c = new Cell();
c.ParsePosition(br);
Cells.Add(c);
}
br.BaseStream.Seek(startPos + _cellPlaybackOffset, SeekOrigin.Begin);
for (int cellNum = 0; cellNum < _cellCount; cellNum++)
{
Cells[cellNum].ParsePlayback(br);
}
br.BaseStream.Seek(startPos + _programMapOffset, SeekOrigin.Begin);
var cellNumbers = new List<int>();
for (int progNum = 0; progNum < _programCount; progNum++) cellNumbers.Add(br.ReadByte() - 1);
for (int i = 0; i < cellNumbers.Count; i++)
{
int max = (i + 1 == cellNumbers.Count) ? _cellCount : cellNumbers[i + 1];
Programs.Add(new Program(Cells.Where((c, idx) => idx >= cellNumbers[i] && idx < max).ToList()));
}
}
}
}

View File

@ -1,70 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.IO;
namespace DvdLib.Ifo
{
public class Title
{
public uint TitleNumber { get; private set; }
public uint AngleCount { get; private set; }
public ushort ChapterCount { get; private set; }
public byte VideoTitleSetNumber { get; private set; }
private ushort _parentalManagementMask;
private byte _titleNumberInVTS;
private uint _vtsStartSector; // relative to start of entire disk
public ProgramChain EntryProgramChain { get; private set; }
public readonly List<ProgramChain> ProgramChains;
public readonly List<Chapter> Chapters;
public Title(uint titleNum)
{
ProgramChains = new List<ProgramChain>();
Chapters = new List<Chapter>();
Chapters = new List<Chapter>();
TitleNumber = titleNum;
}
public bool IsVTSTitle(uint vtsNum, uint vtsTitleNum)
{
return (vtsNum == VideoTitleSetNumber && vtsTitleNum == _titleNumberInVTS);
}
internal void ParseTT_SRPT(BinaryReader br)
{
byte titleType = br.ReadByte();
// TODO parse Title Type
AngleCount = br.ReadByte();
ChapterCount = br.ReadUInt16();
_parentalManagementMask = br.ReadUInt16();
VideoTitleSetNumber = br.ReadByte();
_titleNumberInVTS = br.ReadByte();
_vtsStartSector = br.ReadUInt32();
}
internal void AddPgc(BinaryReader br, long startByte, bool entryPgc, uint pgcNum)
{
long curPos = br.BaseStream.Position;
br.BaseStream.Seek(startByte, SeekOrigin.Begin);
var pgc = new ProgramChain(pgcNum);
pgc.ParseHeader(br);
ProgramChains.Add(pgc);
if (entryPgc)
{
EntryProgramChain = pgc;
}
br.BaseStream.Seek(curPos, SeekOrigin.Begin);
}
}
}

View File

@ -1,37 +0,0 @@
#pragma warning disable CS1591
using System;
namespace DvdLib.Ifo
{
[Flags]
public enum UserOperation
{
None = 0,
TitleOrTimePlay = 1,
ChapterSearchOrPlay = 2,
TitlePlay = 4,
Stop = 8,
GoUp = 16,
TimeOrChapterSearch = 32,
PrevOrTopProgramSearch = 64,
NextProgramSearch = 128,
ForwardScan = 256,
BackwardScan = 512,
TitleMenuCall = 1024,
RootMenuCall = 2048,
SubpictureMenuCall = 4096,
AudioMenuCall = 8192,
AngleMenuCall = 16384,
ChapterMenuCall = 32768,
Resume = 65536,
ButtonSelectOrActive = 131072,
StillOff = 262144,
PauseOn = 524288,
AudioStreamChange = 1048576,
SubpictureStreamChange = 2097152,
AngleChange = 4194304,
KaraokeAudioPresentationModeChange = 8388608,
VideoPresentationModeChange = 16777216,
}
}

View File

@ -1,21 +0,0 @@
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("DvdLib")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

View File

@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
var list = new List<StateVariable>
return new StateVariable[]
{
new StateVariable
{
@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
SendsEvents = false
}
};
return list;
}
}
}

View File

@ -141,7 +141,7 @@ namespace Emby.Dlna.ContentDirectory
{
var user = _userManager.GetUserById(Guid.Parse(profile.UserId));
if (user != null)
if (user is not null)
{
return user;
}
@ -153,7 +153,7 @@ namespace Emby.Dlna.ContentDirectory
{
var user = _userManager.GetUserById(Guid.Parse(userId));
if (user != null)
if (user is not null)
{
return user;
}

View File

@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
var list = new List<StateVariable>
return new StateVariable[]
{
new StateVariable
{
@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
SendsEvents = false
}
};
return list;
}
}
}

View File

@ -1048,7 +1048,7 @@ namespace Emby.Dlna.ContentDirectory
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i is not null).ToArray();
return ToResult(query.StartIndex, items);
}

View File

@ -10,6 +10,7 @@ using System.Text;
using System.Xml;
using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@ -153,7 +154,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("id", clientId);
if (context != null)
if (context is not null)
{
writer.WriteAttributeString("parentID", GetClientId(context, contextStubType));
}
@ -191,11 +192,11 @@ namespace Emby.Dlna.Didl
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
{
if (streamInfo == null)
if (streamInfo is null)
{
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user);
streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
{
ItemId = video.Id,
MediaSources = sources.ToArray(),
@ -263,7 +264,7 @@ namespace Emby.Dlna.Didl
.FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase)
&& i.Method == SubtitleDeliveryMethod.External);
if (subtitleProfile == null)
if (subtitleProfile is null)
{
return false;
}
@ -392,7 +393,7 @@ namespace Emby.Dlna.Didl
var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal));
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
var mimeType = mediaProfile is null || string.IsNullOrEmpty(mediaProfile.MimeType)
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
@ -533,11 +534,11 @@ namespace Emby.Dlna.Didl
{
writer.WriteStartElement(string.Empty, "res", NsDidl);
if (streamInfo == null)
if (streamInfo is null)
{
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user);
streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
{
ItemId = audio.Id,
MediaSources = sources.ToArray(),
@ -598,7 +599,7 @@ namespace Emby.Dlna.Didl
var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal));
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
var mimeType = mediaProfile is null || string.IsNullOrEmpty(mediaProfile.MimeType)
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
@ -652,7 +653,7 @@ namespace Emby.Dlna.Didl
{
writer.WriteAttributeString("id", clientId);
if (context != null)
if (context is not null)
{
writer.WriteAttributeString("parentID", GetClientId(context, null));
}
@ -695,13 +696,13 @@ namespace Emby.Dlna.Didl
}
// Not a samsung device
if (secAttribute == null)
if (secAttribute is null)
{
return;
}
var userdata = _userDataManager.GetUserData(user, item);
var playbackPositionTicks = (streamInfo != null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks;
var playbackPositionTicks = (streamInfo is not null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks;
if (playbackPositionTicks > 0)
{
@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
var types = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer,
PersonType.Composer,
"creator"
PersonKind.Director,
PersonKind.Writer,
PersonKind.Producer,
PersonKind.Composer,
PersonKind.Creator
};
// Seeing some LG models locking up due content with large lists of people
@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl
foreach (var actor in people)
{
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
?? PersonType.Actor;
var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
if (type == PersonKind.Unknown)
{
type = PersonKind.Actor;
}
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
}
}
@ -909,14 +913,14 @@ namespace Emby.Dlna.Didl
AddValue(writer, "dc", "creator", artist, NsDc);
// If it doesn't support album artists (musicvideo), then tag as both
if (hasAlbumArtists == null)
if (hasAlbumArtists is null)
{
AddAlbumArtist(writer, artist);
}
}
}
if (hasAlbumArtists != null)
if (hasAlbumArtists is not null)
{
foreach (var albumArtist in hasAlbumArtists.AlbumArtists)
{
@ -973,7 +977,7 @@ namespace Emby.Dlna.Didl
{
ImageDownloadInfo imageInfo = GetImageInfo(item);
if (imageInfo == null)
if (imageInfo is null)
{
return;
}
@ -1036,7 +1040,7 @@ namespace Emby.Dlna.Didl
{
var imageInfo = GetImageInfo(item);
if (imageInfo == null)
if (imageInfo is null)
{
return;
}
@ -1093,7 +1097,7 @@ namespace Emby.Dlna.Didl
if (item is Audio audioItem)
{
var album = audioItem.AlbumEntity;
return album != null && album.HasImage(ImageType.Primary)
return album is not null && album.HasImage(ImageType.Primary)
? GetImageInfo(album, ImageType.Primary)
: null;
}
@ -1106,7 +1110,7 @@ namespace Emby.Dlna.Didl
// For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item.
var parentWithImage = GetFirstParentWithImageBelowUserRoot(item);
if (parentWithImage != null)
if (parentWithImage is not null)
{
return GetImageInfo(parentWithImage, ImageType.Primary);
}
@ -1116,7 +1120,7 @@ namespace Emby.Dlna.Didl
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
{
if (item == null)
if (item is null)
{
return null;
}

View File

@ -105,9 +105,9 @@ namespace Emby.Dlna
ArgumentNullException.ThrowIfNull(deviceInfo);
var profile = GetProfiles()
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
.FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
if (profile == null)
if (profile is null)
{
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
}
@ -171,8 +171,8 @@ namespace Emby.Dlna
{
ArgumentNullException.ThrowIfNull(headers);
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
if (profile == null)
var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
if (profile is null)
{
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
}
@ -199,6 +199,11 @@ namespace Emby.Dlna
if (headers.TryGetValue(header.Name, out StringValues value))
{
if (StringValues.IsNullOrEmpty(value))
{
return false;
}
switch (header.Match)
{
case HeaderMatchType.Equals:
@ -208,7 +213,8 @@ namespace Emby.Dlna
// _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
return isMatch;
case HeaderMatchType.Regex:
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
// Can't be null, we checked above the switch statement
return Regex.IsMatch(value!, header.Value, RegexOptions.IgnoreCase);
default:
throw new ArgumentException("Unrecognized HeaderMatchType");
}
@ -224,7 +230,7 @@ namespace Emby.Dlna
return _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
.Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls
}
catch (IOException)
@ -265,14 +271,11 @@ namespace Emby.Dlna
/// <inheritdoc />
public DeviceProfile? GetProfile(string id)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
ArgumentException.ThrowIfNullOrEmpty(id);
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
if (info == null)
if (info is null)
{
return null;
}
@ -371,10 +374,7 @@ namespace Emby.Dlna
{
profile = ReserializeProfile(profile);
if (string.IsNullOrEmpty(profile.Name))
{
throw new ArgumentException("Profile is missing Name");
}
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename);
@ -387,15 +387,9 @@ namespace Emby.Dlna
{
profile = ReserializeProfile(profile);
if (string.IsNullOrEmpty(profile.Id))
{
throw new ArgumentException("Profile is missing Id");
}
ArgumentException.ThrowIfNullOrEmpty(profile.Id);
if (string.IsNullOrEmpty(profile.Name))
{
throw new ArgumentException("Profile is missing Name");
}
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
if (current.Info.Type == DeviceProfileType.System)
@ -470,7 +464,7 @@ namespace Emby.Dlna
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
var stream = _assembly.GetManifestResourceStream(resource);
if (stream == null)
if (stream is null)
{
return null;
}

View File

@ -17,24 +17,24 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@ -80,7 +80,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>

View File

@ -35,7 +35,7 @@ namespace Emby.Dlna.Eventing
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
{
var subscription = GetSubscription(subscriptionId, false);
if (subscription != null)
if (subscription is not null)
{
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
int timeoutSeconds = subscription.TimeoutSeconds;
@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
try
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
}
catch (OperationCanceledException)

View File

@ -1,3 +1,4 @@
#nullable disable
#pragma warning disable CS1591
namespace Emby.Dlna

View File

@ -199,7 +199,7 @@ namespace Emby.Dlna.Main
{
try
{
if (_communicationsServer == null)
if (_communicationsServer is null)
{
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
OperatingSystem.IsLinux();
@ -222,7 +222,7 @@ namespace Emby.Dlna.Main
{
try
{
if (communicationsServer != null)
if (communicationsServer is not null)
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
@ -253,7 +253,7 @@ namespace Emby.Dlna.Main
return;
}
if (_publisher != null)
if (_publisher is not null)
{
return;
}
@ -262,7 +262,7 @@ namespace Emby.Dlna.Main
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.Platform.ToString(),
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
@ -382,7 +382,7 @@ namespace Emby.Dlna.Main
{
lock (_syncLock)
{
if (_manager != null)
if (_manager is not null)
{
return;
}
@ -417,7 +417,7 @@ namespace Emby.Dlna.Main
{
lock (_syncLock)
{
if (_manager != null)
if (_manager is not null)
{
try
{
@ -436,7 +436,7 @@ namespace Emby.Dlna.Main
public void DisposeDevicePublisher()
{
if (_publisher != null)
if (_publisher is not null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
@ -456,7 +456,7 @@ namespace Emby.Dlna.Main
DisposePlayToManager();
DisposeDeviceDiscovery();
if (_communicationsServer != null)
if (_communicationsServer is not null)
{
_logger.LogInformation("Disposing SsdpCommunicationsServer");
_communicationsServer.Dispose();

View File

@ -220,14 +220,14 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null)
if (command is null)
{
return false;
}
var service = GetServiceRenderingControl();
if (service == null)
if (service is null)
{
return false;
}
@ -260,14 +260,14 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null)
if (command is null)
{
return;
}
var service = GetServiceRenderingControl();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
@ -291,14 +291,14 @@ namespace Emby.Dlna.PlayTo
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null)
if (command is null)
{
return;
}
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
@ -324,7 +324,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");
if (command == null)
if (command is null)
{
return;
}
@ -337,7 +337,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
@ -381,7 +381,7 @@ namespace Emby.Dlna.PlayTo
_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)
if (command is null)
{
return;
}
@ -394,7 +394,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
@ -418,13 +418,13 @@ namespace Emby.Dlna.PlayTo
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play");
if (command == null)
if (command is null)
{
return Task.CompletedTask;
}
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
@ -440,7 +440,7 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
if (avCommands == null)
if (avCommands is null)
{
return;
}
@ -455,7 +455,7 @@ namespace Emby.Dlna.PlayTo
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null)
if (command is null)
{
return;
}
@ -479,7 +479,7 @@ namespace Emby.Dlna.PlayTo
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null)
if (command is null)
{
return;
}
@ -513,7 +513,7 @@ namespace Emby.Dlna.PlayTo
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
if (avCommands == null)
if (avCommands is null)
{
return;
}
@ -538,12 +538,12 @@ namespace Emby.Dlna.PlayTo
var currentObject = tuple.Track;
if (tuple.Success && currentObject == null)
if (tuple.Success && currentObject is null)
{
currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false);
}
if (currentObject != null)
if (currentObject is not null)
{
UpdateMediaInfo(currentObject, transportState.Value);
}
@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo
if (_connectFailureCount >= 3)
{
var action = OnDeviceUnavailable;
if (action != null)
if (action is not null)
{
_logger.LogDebug("Disposing device due to loss of connection");
action();
@ -607,14 +607,14 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null)
if (command is null)
{
return;
}
var service = GetServiceRenderingControl();
if (service == null)
if (service is null)
{
return;
}
@ -626,12 +626,12 @@ namespace Emby.Dlna.PlayTo
rendererCommands.BuildPost(command, service.ServiceType),
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null)
if (result is null || result.Document is null)
{
return;
}
var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i is not null);
var volumeValue = volume?.Value;
if (string.IsNullOrWhiteSpace(volumeValue))
@ -657,14 +657,14 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null)
if (command is null)
{
return;
}
var service = GetServiceRenderingControl();
if (service == null)
if (service is null)
{
return;
}
@ -676,14 +676,14 @@ namespace Emby.Dlna.PlayTo
rendererCommands.BuildPost(command, service.ServiceType),
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null)
if (result is null || result.Document is null)
{
return;
}
var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
.Select(i => i.Element("CurrentMute"))
.FirstOrDefault(i => i != null);
.FirstOrDefault(i => i is not null);
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
}
@ -691,13 +691,13 @@ namespace Emby.Dlna.PlayTo
private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
if (command == null)
if (command is null)
{
return null;
}
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
return null;
}
@ -709,17 +709,17 @@ namespace Emby.Dlna.PlayTo
avCommands.BuildPost(command, service.ServiceType),
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null)
if (result is null || result.Document is null)
{
return null;
}
var transportState =
result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i is not null);
var transportStateValue = transportState?.Value;
if (transportStateValue != null
if (transportStateValue is not null
&& Enum.TryParse(transportStateValue, true, out TransportState state))
{
return state;
@ -731,19 +731,19 @@ namespace Emby.Dlna.PlayTo
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command == null)
if (command is null)
{
return null;
}
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
if (rendererCommands is null)
{
return null;
}
@ -755,14 +755,14 @@ namespace Emby.Dlna.PlayTo
rendererCommands.BuildPost(command, service.ServiceType),
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null)
if (result is null || result.Document is null)
{
return null;
}
var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault();
if (track == null)
if (track is null)
{
return null;
}
@ -778,7 +778,7 @@ namespace Emby.Dlna.PlayTo
track = result.Document.Descendants("CurrentURI").FirstOrDefault();
if (track == null)
if (track is null)
{
return null;
}
@ -801,21 +801,21 @@ namespace Emby.Dlna.PlayTo
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command == null)
if (command is null)
{
return (false, null);
}
var service = GetAvTransportService();
if (service == null)
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
if (rendererCommands is null)
{
return (false, null);
}
@ -827,15 +827,15 @@ namespace Emby.Dlna.PlayTo
rendererCommands.BuildPost(command, service.ServiceType),
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null)
if (result is null || result.Document is null)
{
return (false, null);
}
var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i is not null);
var trackUri = trackUriElem?.Value;
var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i is not null);
var duration = durationElem?.Value;
if (!string.IsNullOrWhiteSpace(duration)
@ -848,7 +848,7 @@ namespace Emby.Dlna.PlayTo
Duration = null;
}
var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i is not null);
var position = positionElem?.Value;
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@ -858,7 +858,7 @@ namespace Emby.Dlna.PlayTo
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
if (track == null)
if (track is null)
{
// If track is null, some vendors do this, use GetMediaInfo instead.
return (true, null);
@ -882,7 +882,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogError(ex, "Uncaught exception while parsing xml");
}
if (uPnpResponse == null)
if (uPnpResponse is null)
{
_logger.LogError("Failed to parse xml: \n {Xml}", trackString);
return (true, null);
@ -959,11 +959,11 @@ namespace Emby.Dlna.PlayTo
var resElement = container.Element(UPnpNamespaces.Res);
if (resElement != null)
if (resElement is not null)
{
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
if (info != null && !string.IsNullOrWhiteSpace(info.Value))
if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
{
return info.Value.Split(':');
}
@ -974,7 +974,7 @@ namespace Emby.Dlna.PlayTo
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
{
if (AvCommands != null)
if (AvCommands is not null)
{
return AvCommands;
}
@ -985,7 +985,7 @@ namespace Emby.Dlna.PlayTo
}
var avService = GetAvTransportService();
if (avService == null)
if (avService is null)
{
return null;
}
@ -995,7 +995,7 @@ namespace Emby.Dlna.PlayTo
var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
if (document is null)
{
return null;
}
@ -1006,7 +1006,7 @@ namespace Emby.Dlna.PlayTo
private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
{
if (RendererCommands != null)
if (RendererCommands is not null)
{
return RendererCommands;
}
@ -1017,17 +1017,14 @@ namespace Emby.Dlna.PlayTo
}
var avService = GetServiceRenderingControl();
if (avService == null)
{
throw new ArgumentException("Device AvService is null");
}
ArgumentNullException.ThrowIfNull(avService);
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
if (document is null)
{
return null;
}
@ -1062,7 +1059,7 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null)
if (document is null)
{
return null;
}
@ -1070,13 +1067,13 @@ namespace Emby.Dlna.PlayTo
var friendlyNames = new List<string>();
var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
if (name != null && !string.IsNullOrWhiteSpace(name.Value))
if (name is not null && !string.IsNullOrWhiteSpace(name.Value))
{
friendlyNames.Add(name.Value);
}
var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
if (room != null && !string.IsNullOrWhiteSpace(room.Value))
if (room is not null && !string.IsNullOrWhiteSpace(room.Value))
{
friendlyNames.Add(room.Value);
}
@ -1088,74 +1085,74 @@ namespace Emby.Dlna.PlayTo
};
var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
if (model != null)
if (model is not null)
{
deviceProperties.ModelName = model.Value;
}
var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
if (modelNumber != null)
if (modelNumber is not null)
{
deviceProperties.ModelNumber = modelNumber.Value;
}
var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
if (uuid != null)
if (uuid is not null)
{
deviceProperties.UUID = uuid.Value;
}
var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
if (manufacturer != null)
if (manufacturer is not null)
{
deviceProperties.Manufacturer = manufacturer.Value;
}
var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
if (manufacturerUrl != null)
if (manufacturerUrl is not null)
{
deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
}
var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
if (presentationUrl != null)
if (presentationUrl is not null)
{
deviceProperties.PresentationUrl = presentationUrl.Value;
}
var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
if (modelUrl != null)
if (modelUrl is not null)
{
deviceProperties.ModelUrl = modelUrl.Value;
}
var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
if (serialNumber != null)
if (serialNumber is not null)
{
deviceProperties.SerialNumber = serialNumber.Value;
}
var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
if (modelDescription != null)
if (modelDescription is not null)
{
deviceProperties.ModelDescription = modelDescription.Value;
}
var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
if (icon != null)
if (icon is not null)
{
deviceProperties.Icon = CreateIcon(icon);
}
foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
{
if (services == null)
if (services is null)
{
continue;
}
var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
if (servicesList == null)
if (servicesList is null)
{
continue;
}
@ -1164,7 +1161,7 @@ namespace Emby.Dlna.PlayTo
{
var service = Create(element);
if (service != null)
if (service is not null)
{
deviceProperties.Services.Add(service);
}
@ -1212,14 +1209,14 @@ namespace Emby.Dlna.PlayTo
var previousMediaInfo = CurrentMediaInfo;
CurrentMediaInfo = mediaInfo;
if (mediaInfo == null)
if (mediaInfo is null)
{
if (previousMediaInfo != null)
if (previousMediaInfo is not null)
{
OnPlaybackStop(previousMediaInfo);
}
}
else if (previousMediaInfo == null)
else if (previousMediaInfo is null)
{
if (state != TransportState.STOPPED)
{

View File

@ -2,9 +2,11 @@
using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo
{
public class DlnaHttpClient
/// <summary>
/// Http client for Dlna PlayTo function.
/// </summary>
public partial class DlnaHttpClient
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
@ -44,25 +49,44 @@ namespace Emby.Dlna.PlayTo
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using MemoryStream ms = new MemoryStream();
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
ms,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch (XmlException ex)
catch (XmlException)
{
_logger.LogError(ex, "Failed to parse response");
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
}
// try correcting the Xml response with common errors
ms.Position = 0;
using StreamReader sr = new StreamReader(ms);
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
return null;
// find and replace unescaped ampersands (&)
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
try
{
// retry reading Xml
using var xmlReader = new StringReader(xmlString);
return await XDocument.LoadAsync(
xmlReader,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch (XmlException ex)
{
_logger.LogError(ex, "Failed to parse response");
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
return null;
}
}
}
@ -104,5 +128,12 @@ namespace Emby.Dlna.PlayTo
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Compile-time generated regular expression for escaping ampersands.
/// </summary>
/// <returns>Compiled regular expression.</returns>
[GeneratedRegex("(&(?![a-z]*;))")]
private static partial Regex EscapeAmpersandRegex();
}
}

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder)
IMediaEncoder mediaEncoder,
Device device)
{
_session = session;
_sessionManager = sessionManager;
@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
}
public bool IsSessionActive => !_disposed && _device != null;
public bool SupportsMediaControl => IsSessionActive;
public void Init(Device device)
{
_device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable;
_device.PlaybackStart += OnDevicePlaybackStart;
@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
public bool IsSessionActive => !_disposed;
public bool SupportsMediaControl => IsSessionActive;
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
}
}
private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{
var info = e.Argument;
if (!_disposed
&& info.Headers.TryGetValue("USN", out string usn)
&& info.Headers.TryGetValue("USN", out string? usn)
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|| (info.Headers.TryGetValue("NT", out string nt)
|| (info.Headers.TryGetValue("NT", out string? nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
{
OnDeviceUnavailable();
}
}
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
{
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
@ -156,7 +152,7 @@ namespace Emby.Dlna.PlayTo
try
{
var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
if (streamInfo.Item != null)
if (streamInfo.Item is not null)
{
var positionTicks = GetProgressPositionTicks(streamInfo);
@ -164,7 +160,7 @@ namespace Emby.Dlna.PlayTo
}
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
if (streamInfo.Item == null)
if (streamInfo.Item is null)
{
return;
}
@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
}
}
private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
{
if (_disposed)
{
@ -199,7 +195,7 @@ namespace Emby.Dlna.PlayTo
{
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
if (streamInfo.Item == null)
if (streamInfo.Item is null)
{
return;
}
@ -210,7 +206,7 @@ namespace Emby.Dlna.PlayTo
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
var duration = mediaSource == null
var duration = mediaSource is null
? _device.Duration?.Ticks
: mediaSource.RunTimeTicks;
@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
}
}
private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
{
if (_disposed)
{
@ -268,7 +264,7 @@ namespace Emby.Dlna.PlayTo
{
var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
if (info.Item != null)
if (info.Item is not null)
{
var progress = GetProgressInfo(info);
@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
}
}
private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (_disposed)
{
@ -299,7 +295,7 @@ namespace Emby.Dlna.PlayTo
var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager);
if (info.Item != null)
if (info.Item is not null)
{
var progress = GetProgressInfo(info);
@ -338,7 +334,6 @@ namespace Emby.Dlna.PlayTo
SubtitleStreamIndex = info.SubtitleStreamIndex,
VolumeLevel = _device.Volume,
// TODO
CanSeek = true,
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
@ -442,11 +437,11 @@ namespace Emby.Dlna.PlayTo
{
var media = _device.CurrentMediaInfo;
if (media != null)
if (media is not null)
{
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
if (info.Item != null && !EnableClientSideSeek(info))
if (info.Item is not null && !EnableClientSideSeek(info))
{
var user = _session.UserId.Equals(default)
? null
@ -487,9 +482,9 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem CreatePlaylistItem(
BaseItem item,
User user,
User? user,
long startPostionTicks,
string mediaSourceId,
string? mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{
@ -526,7 +521,7 @@ namespace Emby.Dlna.PlayTo
return playlistItem;
}
private string GetDlnaHeaders(PlaylistItem item)
private string? GetDlnaHeaders(PlaylistItem item)
{
var profile = item.Profile;
var streamInfo = item.StreamInfo;
@ -580,13 +575,13 @@ namespace Emby.Dlna.PlayTo
return null;
}
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
return new PlaylistItem
{
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
{
ItemId = item.Id,
MediaSources = mediaSources,
@ -606,7 +601,7 @@ namespace Emby.Dlna.PlayTo
{
return new PlaylistItem
{
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
{
ItemId = item.Id,
MediaSources = mediaSources,
@ -697,7 +692,6 @@ namespace Emby.Dlna.PlayTo
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
_device = null;
_disposed = true;
}
@ -717,7 +711,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
if (command.Arguments.TryGetValue("Index", out string? index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
@ -741,7 +735,7 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
if (command.Arguments.TryGetValue("Volume", out string? vol))
{
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
@ -761,11 +755,11 @@ namespace Emby.Dlna.PlayTo
{
var media = _device.CurrentMediaInfo;
if (media != null)
if (media is not null)
{
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
if (info.Item != null)
if (info.Item is not null)
{
var newPosition = GetProgressPositionTicks(info) ?? 0;
@ -792,11 +786,11 @@ namespace Emby.Dlna.PlayTo
{
var media = _device.CurrentMediaInfo;
if (media != null)
if (media is not null)
{
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
if (info.Item != null)
if (info.Item is not null)
{
var newPosition = GetProgressPositionTicks(info) ?? 0;
@ -866,34 +860,19 @@ namespace Emby.Dlna.PlayTo
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
return name switch
{
return Task.CompletedTask;
}
if (name == SessionMessageType.Play)
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (name == SessionMessageType.Playstate)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (name == SessionMessageType.GeneralCommand)
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
_ => Task.CompletedTask // Not supported or needed right now
};
}
private class StreamParams
{
private MediaSourceInfo _mediaSource;
private IMediaSourceManager _mediaSourceManager;
private MediaSourceInfo? _mediaSource;
private IMediaSourceManager? _mediaSourceManager;
public Guid ItemId { get; set; }
@ -905,19 +884,19 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; }
public string DeviceProfileId { get; set; }
public string? DeviceProfileId { get; set; }
public string DeviceId { get; set; }
public string? DeviceId { get; set; }
public string MediaSourceId { get; set; }
public string? MediaSourceId { get; set; }
public string LiveStreamId { get; set; }
public string? LiveStreamId { get; set; }
public BaseItem Item { get; set; }
public BaseItem? Item { get; set; }
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
{
if (_mediaSource != null)
if (_mediaSource is not null)
{
return _mediaSource;
}
@ -927,7 +906,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
if (_mediaSourceManager != null)
if (_mediaSourceManager is not null)
{
_mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
}
@ -937,10 +916,7 @@ namespace Emby.Dlna.PlayTo
private static Guid GetItemId(string url)
{
if (string.IsNullOrEmpty(url))
{
throw new ArgumentNullException(nameof(url));
}
ArgumentException.ThrowIfNullOrEmpty(url);
var parts = url.Split('/');
@ -948,8 +924,8 @@ namespace Emby.Dlna.PlayTo
{
var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
if (Guid.TryParse(parts[i + 1], out var result))
{
@ -963,10 +939,7 @@ namespace Emby.Dlna.PlayTo
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
{
if (string.IsNullOrEmpty(url))
{
throw new ArgumentNullException(nameof(url));
}
ArgumentException.ThrowIfNullOrEmpty(url);
var request = new StreamParams
{

View File

@ -176,10 +176,10 @@ namespace Emby.Dlna.PlayTo
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
if (controller == null)
if (controller is null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
if (device == null)
if (device is null)
{
_logger.LogError("Ignoring device as xml response is invalid.");
return;
@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
_userDataManager,
_localization,
_mediaSourceManager,
_mediaEncoder);
_mediaEncoder,
device);
sessionInfo.AddController(controller);
controller.Init(device);
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile();

View File

@ -29,7 +29,7 @@ namespace Emby.Dlna.PlayTo
var directPlay = profile.DirectPlayProfiles
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item));
if (directPlay != null)
if (directPlay is not null)
{
playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream;
playlistItem.StreamInfo.Container = Path.GetExtension(item.Path);
@ -40,7 +40,7 @@ namespace Emby.Dlna.PlayTo
var transcodingProfile = profile.TranscodingProfiles
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo);
if (transcodingProfile != null)
if (transcodingProfile is not null)
{
playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode;
playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');

View File

@ -31,7 +31,7 @@ namespace Emby.Dlna.PlayTo
var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
if (stateValues != null)
if (stateValues is not null)
{
foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
{
@ -77,7 +77,7 @@ namespace Emby.Dlna.PlayTo
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
.FirstOrDefault();
if (element != null)
if (element is not null)
{
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
{
var stateString = string.Empty;
@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
{
var stateString = string.Empty;
@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
{
stateString += BuildArgumentXml(arg, "0");
}
else if (dictionary.ContainsKey(arg.Name))
else if (dictionary.TryGetValue(arg.Name, out var argValue))
{
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
stateString += BuildArgumentXml(arg, argValue);
}
else
{
@ -160,14 +160,14 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
{
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
if (state != null)
if (state is not null)
{
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);

View File

@ -92,6 +92,12 @@ namespace Emby.Dlna.Profiles
Method = SubtitleDeliveryMethod.External,
},
new SubtitleProfile
{
Format = "sup",
Method = SubtitleDeliveryMethod.External
},
new SubtitleProfile
{
Format = "srt",
@ -140,6 +146,12 @@ namespace Emby.Dlna.Profiles
Method = SubtitleDeliveryMethod.Embed
},
new SubtitleProfile
{
Format = "sup",
Method = SubtitleDeliveryMethod.Embed
},
new SubtitleProfile
{
Format = "subrip",

View File

@ -22,15 +22,8 @@ namespace Emby.Dlna.Server
public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId)
{
if (string.IsNullOrEmpty(serverUdn))
{
throw new ArgumentNullException(nameof(serverUdn));
}
if (string.IsNullOrEmpty(serverAddress))
{
throw new ArgumentNullException(nameof(serverAddress));
}
ArgumentException.ThrowIfNullOrEmpty(serverUdn);
ArgumentException.ThrowIfNullOrEmpty(serverAddress);
_profile = profile;
_serverUdn = serverUdn;
@ -154,11 +147,16 @@ namespace Emby.Dlna.Server
}
}
private string GetFriendlyName()
internal string GetFriendlyName()
{
if (string.IsNullOrEmpty(_profile.FriendlyName))
{
return "Jellyfin - " + _serverName;
return _serverName;
}
if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
{
return _profile.FriendlyName;
}
var characterList = new List<char>();
@ -171,13 +169,18 @@ namespace Emby.Dlna.Server
}
}
var characters = characterList.ToArray();
var serverName = string.Create(
characterList.Count,
characterList,
(dest, source) =>
{
for (int i = 0; i < dest.Length; i++)
{
dest[i] = source[i];
}
});
var serverName = new string(characters);
var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
return name ?? string.Empty;
return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
}
private void AppendIconList(StringBuilder builder)

View File

@ -173,7 +173,7 @@ namespace Emby.Dlna.Service
}
}
if (localName != null && namespaceURI != null)
if (localName is not null && namespaceURI is not null)
{
return new ControlRequestInfo(localName, namespaceURI);
}

View File

@ -1,3 +1,4 @@
#nullable disable
#pragma warning disable CS1591
using System.Net.Http;

View File

@ -71,7 +71,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);
@ -97,7 +97,7 @@ namespace Emby.Dlna.Ssdp
{
var originalHeaders = e.DiscoveredDevice.ResponseHeaders;
var headerDict = originalHeaders == null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
@ -116,7 +116,7 @@ namespace Emby.Dlna.Ssdp
{
var originalHeaders = e.DiscoveredDevice.ResponseHeaders;
var headerDict = originalHeaders == null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
@ -136,7 +136,7 @@ namespace Emby.Dlna.Ssdp
if (!_disposed)
{
_disposed = true;
if (_deviceLocator != null)
if (_deviceLocator is not null)
{
_deviceLocator.Dispose();
_deviceLocator = null;

View File

@ -1,580 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Drawing
{
/// <summary>
/// Class ImageProcessor.
/// </summary>
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
private const char Version = '3';
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
private readonly ILogger<ImageProcessor> _logger;
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
private readonly IMediaEncoder _mediaEncoder;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ImageProcessor"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="appPaths">The server application paths.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="mediaEncoder">The media encoder.</param>
public ImageProcessor(
ILogger<ImageProcessor> logger,
IServerApplicationPaths appPaths,
IFileSystem fileSystem,
IImageEncoder imageEncoder,
IMediaEncoder mediaEncoder)
{
_logger = logger;
_fileSystem = fileSystem;
_imageEncoder = imageEncoder;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"tiff",
"tif",
"jpeg",
"jpg",
"png",
"aiff",
"cr2",
"crw",
"nef",
"orf",
"pef",
"arw",
"webp",
"gif",
"bmp",
"erf",
"raf",
"rw2",
"nrw",
"dng",
"ico",
"astc",
"ktx",
"pkm",
"wbmp"
};
/// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc />
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{
var file = await ProcessImage(options).ConfigureAwait(false);
using (var fileStream = AsyncFile.OpenRead(file.Path))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
}
/// <inheritdoc />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats;
/// <inheritdoc />
public bool SupportsTransparency(string path)
=> _transparentImageTypes.Contains(Path.GetExtension(path));
/// <inheritdoc />
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
{
ItemImageInfo originalImage = options.Image;
BaseItem item = options.Item;
string originalImagePath = originalImage.Path;
DateTime dateModified = originalImage.DateModified;
ImageDimensions? originalImageSize = null;
if (originalImage.Width > 0 && originalImage.Height > 0)
{
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
}
var mimeType = MimeTypes.GetMimeType(originalImagePath);
if (!_imageEncoder.SupportsImageEncoding)
{
return (originalImagePath, mimeType, dateModified);
}
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
originalImagePath = supportedImageInfo.Path;
// Original file doesn't exist, or original file is gif.
if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
{
return (originalImagePath, mimeType, dateModified);
}
dateModified = supportedImageInfo.DateModified;
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
bool autoOrient = false;
ImageOrientation? orientation = null;
if (item is Photo photo)
{
if (photo.Orientation.HasValue)
{
if (photo.Orientation.Value != ImageOrientation.TopLeft)
{
autoOrient = true;
orientation = photo.Orientation;
}
}
else
{
// Orientation unknown, so do it
autoOrient = true;
orientation = photo.Orientation;
}
}
if (options.HasDefaultOptions(originalImagePath, originalImageSize) && (!autoOrient || !options.RequiresAutoOrientation))
{
// Just spit out the original file if all the options are default
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
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))
{
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
{
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
}
return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
}
catch (Exception ex)
{
// If it fails for whatever reason, return the original image
_logger.LogError(ex, "Error encoding image");
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
}
private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
{
var serverFormats = GetSupportedImageOutputFormats();
// Client doesn't care about format, so start with webp if supported
if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp))
{
return ImageFormat.Webp;
}
// If transparency is needed and webp isn't supported, than png is the only option
if (requiresTransparency && clientSupportedFormats.Contains(ImageFormat.Png))
{
return ImageFormat.Png;
}
foreach (var format in clientSupportedFormats)
{
if (serverFormats.Contains(format))
{
return format;
}
}
// We should never actually get here
return ImageFormat.Jpg;
}
private string GetMimeType(ImageFormat format, string path)
=> format switch
{
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
_ => MimeTypes.GetMimeType(path)
};
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </summary>
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 = 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.Append(",pl=true");
}
if (percentPlayed > 0)
{
filename.Append(",p=");
filename.Append(percentPlayed);
}
if (unwatchedCount.HasValue)
{
filename.Append(",p=");
filename.Append(unwatchedCount.Value);
}
if (blur.HasValue)
{
filename.Append(",blur=");
filename.Append(blur.Value);
}
if (!string.IsNullOrEmpty(backgroundColor))
{
filename.Append(",b=");
filename.Append(backgroundColor);
}
if (!string.IsNullOrEmpty(foregroundLayer))
{
filename.Append(",fl=");
filename.Append(foregroundLayer);
}
filename.Append(",v=");
filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
}
/// <inheritdoc />
public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
{
int width = info.Width;
int height = info.Height;
if (height > 0 && width > 0)
{
return new ImageDimensions(width, height);
}
string path = info.Path;
_logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
ImageDimensions size = GetImageDimensions(path);
info.Width = size.Width;
info.Height = size.Height;
return size;
}
/// <inheritdoc />
public ImageDimensions GetImageDimensions(string path)
=> _imageEncoder.GetImageSize(path);
/// <inheritdoc />
public string GetImageBlurHash(string path)
{
var size = GetImageDimensions(path);
return GetImageBlurHash(path, size);
}
/// <inheritdoc />
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
{
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
{
return string.Empty;
}
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
int xComp = Math.Min((int)xCompF + 1, 9);
int yComp = Math.Min((int)yCompF + 1, 9);
return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
}
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
=> (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
{
return GetImageCacheTag(item, new ItemImageInfo
{
Path = chapter.ImagePath,
Type = ImageType.Chapter,
DateModified = chapter.ImageDateModified
});
}
/// <inheritdoc />
public string? GetImageCacheTag(User user)
{
if (user.ProfileImage == null)
{
return null;
}
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture);
}
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{
var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
// These are just jpg files renamed as tbn
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult((originalImagePath, dateModified));
}
// TODO _mediaEncoder.ConvertImage is not implemented
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
// {
// try
// {
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
//
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
//
// var file = _fileSystem.GetFileInfo(outputPath);
// if (!file.Exists)
// {
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
// }
// else
// {
// dateModified = file.LastWriteTimeUtc;
// }
//
// originalImagePath = outputPath;
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
// }
// }
return Task.FromResult((originalImagePath, dateModified));
}
/// <summary>
/// Gets the cache path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="uniqueName">Name of the unique.</param>
/// <param name="fileExtension">The file extension.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">
/// path
/// or
/// uniqueName
/// or
/// fileExtension.
/// </exception>
public string GetCachePath(string path, string uniqueName, string fileExtension)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
if (string.IsNullOrEmpty(uniqueName))
{
throw new ArgumentNullException(nameof(uniqueName));
}
if (string.IsNullOrEmpty(fileExtension))
{
throw new ArgumentNullException(nameof(fileExtension));
}
var filename = uniqueName.GetMD5() + fileExtension;
return GetCachePath(path, filename);
}
/// <summary>
/// Gets the cache path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">
/// path
/// or
/// filename.
/// </exception>
public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
{
if (path.IsEmpty)
{
throw new ArgumentException("Path can't be empty.", nameof(path));
}
if (filename.IsEmpty)
{
throw new ArgumentException("Filename can't be empty.", nameof(filename));
}
var prefix = filename.Slice(0, 1);
return Path.Join(path, prefix, filename);
}
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
if (_imageEncoder is IDisposable disposable)
{
disposable.Dispose();
}
_disposed = true;
}
}
}

View File

@ -1,58 +0,0 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
namespace Emby.Drawing
{
/// <summary>
/// A fallback implementation of <see cref="IImageEncoder" />.
/// </summary>
public class NullImageEncoder : IImageEncoder
{
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats
=> new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" };
/// <inheritdoc />
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png };
/// <inheritdoc />
public string Name => "Null Image Encoder";
/// <inheritdoc />
public bool SupportsImageCollageCreation => false;
/// <inheritdoc />
public bool SupportsImageEncoding => false;
/// <inheritdoc />
public ImageDimensions GetImageSize(string path)
=> throw new NotImplementedException();
/// <inheritdoc />
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public string GetImageBlurHash(int xComp, int yComp, string path)
{
throw new NotImplementedException();
}
}
}

View File

@ -3,6 +3,7 @@ using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
var tmp = trimmedFilename.Slice(prefix.Length).Trim();
int index = tmp.IndexOf(' ');
if (index != -1)
{
tmp = tmp.Slice(0, index);
}
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
{
return true;
}

View File

@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{
var match = new Regex(expression, RegexOptions.IgnoreCase).Match(fileName);
var match = Regex.Match(fileName, expression, RegexOptions.IgnoreCase);
if (match.Success)
{
if (!result.ChapterNumber.HasValue)
@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["chapter"];
if (value.Success)
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.ChapterNumber = intValue;
}
@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["part"];
if (value.Success)
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.PartNumber = intValue;
}

View File

@ -69,35 +69,35 @@ namespace Emby.Naming.AudioBook
extras = new List<AudioBookFileInfo>();
alternativeVersions = new List<AudioBookFileInfo>();
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber is not null || x.PartNumber is not null);
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
foreach (var group in groupedBy)
{
if (group.Key.ChapterNumber == null && group.Key.PartNumber == null)
if (group.Key.ChapterNumber is null && group.Key.PartNumber is null)
{
if (group.Count() > 1 || haveChaptersOrPages)
{
var ex = new List<AudioBookFileInfo>();
var alt = new List<AudioBookFileInfo>();
List<AudioBookFileInfo>? ex = null;
List<AudioBookFileInfo>? alt = null;
foreach (var audioFile in group)
{
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
|| name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
|| name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{
alt.Add(audioFile);
(alt ??= new()).Add(audioFile);
}
else
{
ex.Add(audioFile);
(ex ??= new()).Add(audioFile);
}
}
if (ex.Count > 0)
if (ex is not null)
{
var extra = ex
.OrderBy(x => x.Container)
@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
extras.AddRange(extra);
}
if (alt.Count > 0)
if (alt is not null)
{
var alternatives = alt
.OrderBy(x => x.Container)

View File

@ -30,10 +30,10 @@ namespace Emby.Naming.AudioBook
AudioBookNameParserResult result = default;
foreach (var expression in _options.AudioBookNamesExpressions)
{
var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
var match = Regex.Match(name, expression, RegexOptions.IgnoreCase);
if (match.Success)
{
if (result.Name == null)
if (result.Name is null)
{
var value = match.Groups["name"];
if (value.Success)
@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["year"];
if (value.Success)
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.Year = intValue;
}

View File

@ -141,8 +141,7 @@ namespace Emby.Naming.Common
VideoFileStackingRules = new[]
{
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@ -153,11 +152,12 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
};
SubtitleFileExtensions = new[]
@ -169,6 +169,7 @@ namespace Emby.Naming.Common
".srt",
".ssa",
".sub",
".sup",
".vtt",
};
@ -269,7 +270,6 @@ namespace Emby.Naming.Common
".sfx",
".shn",
".sid",
".spc",
".stm",
".strm",
".ult",
@ -337,7 +337,15 @@ namespace Emby.Naming.Common
}
},
// This isn't a Kodi naming rule, but the expression below causes false positives,
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for
// Title Season X Episode X naming schemes.
// "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi"
new EpisodeExpression(@".*[\\\/]((?<seriesname>[^\\/]+?)\s)?[Ss](?:eason)?\s*(?<seasonnumber>[0-9]+)\s+[Ee](?:pisode)?\s*(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
// Not a Kodi rule as well, but the expression below also causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
@ -452,16 +460,6 @@ namespace Emby.Naming.Common
},
};
EpisodeWithoutSeasonExpressions = new[]
{
@"[/\._ \-]()([0-9]+)(-[0-9]+)?"
};
EpisodeMultiPartExpressions = new[]
{
@"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)"
};
VideoExtraRules = new[]
{
new ExtraRule(
@ -796,16 +794,6 @@ namespace Emby.Naming.Common
/// </summary>
public EpisodeExpression[] EpisodeExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw episode without season regular expressions strings.
/// </summary>
public string[] EpisodeWithoutSeasonExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw multi-part episodes regular expressions strings.
/// </summary>
public string[] EpisodeMultiPartExpressions { get; set; }
/// <summary>
/// Gets or sets list of video file extensions.
/// </summary>
@ -876,16 +864,6 @@ namespace Emby.Naming.Common
/// </summary>
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Gets list of episode without season regular expressions.
/// </summary>
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Gets list of multi-part episode regular expressions.
/// </summary>
public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Compiles raw regex strings into regexes.
/// </summary>
@ -893,8 +871,6 @@ namespace Emby.Naming.Common
{
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
}
private Regex Compile(string exp)

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -16,7 +16,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@ -42,18 +42,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -94,12 +94,12 @@ namespace Emby.Naming.ExternalFiles
// Try to translate to three character code
var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator);
if (culture != null && pathInfo.Language == null)
if (culture is not null && pathInfo.Language is null)
{
pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (culture != null && pathInfo.Language == "hin")
else if (culture is not null && pathInfo.Language == "hin")
{
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
pathInfo.IsHearingImpaired = true;

View File

@ -76,7 +76,7 @@ namespace Emby.Naming.TV
}
}
if (result != null && fillExtendedInfo)
if (result is not null && fillExtendedInfo)
{
FillAdditional(path, result);
@ -113,7 +113,7 @@ namespace Emby.Naming.TV
if (expression.DateTimeFormats.Length > 0)
{
if (DateTime.TryParseExact(
match.Groups[0].Value,
match.Groups[0].ValueSpan,
expression.DateTimeFormats,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true;
}
}
else if (DateTime.TryParse(match.Groups[0].Value, out date))
else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
{
result.Year = date.Year;
result.Month = date.Month;
@ -138,12 +138,12 @@ namespace Emby.Naming.TV
}
else if (expression.IsNamed)
{
if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
@ -158,7 +158,7 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EndingEpisodeNumber = num;
}
@ -170,12 +170,12 @@ namespace Emby.Naming.TV
}
else
{
if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}

View File

@ -28,7 +28,7 @@ namespace Emby.Naming.TV
}
}
if (result != null)
if (result is not null)
{
if (!string.IsNullOrEmpty(result.SeriesName))
{

View File

@ -14,7 +14,7 @@ namespace Emby.Naming.TV
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W".
/// </summary>
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
/// <summary>
/// Resolve information about series from path.

View File

@ -43,7 +43,7 @@ namespace Emby.Naming.Video
&& match.Groups.Count == 5
&& match.Groups[1].Success
&& match.Groups[2].Success
&& int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
&& int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
return true;

View File

@ -56,7 +56,7 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path);
var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -76,7 +76,7 @@ namespace Emby.Naming.Video
}
}
if (result.ExtraType != null)
if (result.ExtraType is not null)
{
return result;
}

View File

@ -17,7 +17,7 @@ public class FileStackRule
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
public FileStackRule(string token, bool isNumerical)
{
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
IsNumerical = isNumerical;
}

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Jellyfin.Extensions;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@ -13,6 +14,8 @@ namespace Emby.Naming.Video
/// </summary>
public static class VideoListResolver
{
private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
@ -26,7 +29,7 @@ namespace Emby.Naming.Video
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
.Where(i => i.ExtraType == null)
.Where(i => i.ExtraType is null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
@ -42,7 +45,7 @@ namespace Emby.Naming.Video
continue;
}
if (current.ExtraType == null)
if (current.ExtraType is null)
{
standaloneMedia.Add(current);
}
@ -106,37 +109,52 @@ namespace Emby.Naming.Video
}
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
VideoInfo? primary = null;
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
if (video.ExtraType != null)
if (video.ExtraType is not null)
{
continue;
}
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
{
return videos;
}
if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
{
primary = video;
}
}
// The list is created and overwritten in the caller, so we are allowed to do in-place sorting
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
if (videos.Count > 1)
{
var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
videos.Clear();
foreach (var group in groups)
{
if (group.Key)
{
videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
}
else
{
videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
}
}
}
primary ??= videos[0];
videos.Remove(primary);
var list = new List<VideoInfo>
{
videos[0]
primary
};
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
}
list[0].AlternateVersions = alternateVersions;
list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
list[0].Name = folderName.ToString();
return list;
@ -161,9 +179,8 @@ namespace Emby.Naming.Video
return true;
}
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
{
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
return false;
@ -176,16 +193,15 @@ namespace Emby.Naming.Video
}
// There are no span overloads for regex unfortunately
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
{
tmpTestFilename = cleanName.Trim();
testFilename = cleanName.AsSpan().Trim();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(tmpTestFilename)
return testFilename.IsEmpty
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
}
}

View File

@ -87,8 +87,7 @@ namespace Emby.Naming.Video
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
&& TryCleanString(name, namingOptions, out var newName))
if (TryCleanString(name, namingOptions, out var newName))
{
name = newName;
}

View File

@ -1,123 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Notifications;
namespace Emby.Notifications
{
public class CoreNotificationTypes : INotificationTypeFactory
{
private readonly ILocalizationManager _localization;
public CoreNotificationTypes(ILocalizationManager localization)
{
_localization = localization;
}
public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
{
var knownTypes = new NotificationTypeInfo[]
{
new NotificationTypeInfo
{
Type = nameof(NotificationType.ApplicationUpdateInstalled)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.InstallationFailed)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginInstalled)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginError)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginUninstalled)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginUpdateInstalled)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.ServerRestartRequired)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.TaskFailed)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.NewLibraryContent)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.AudioPlayback)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.VideoPlayback)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.AudioPlaybackStopped)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.VideoPlaybackStopped)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.UserLockedOut)
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.ApplicationUpdateAvailable)
}
};
foreach (var type in knownTypes)
{
Update(type);
}
var systemName = _localization.GetLocalizedString("System");
return knownTypes.OrderByDescending(i => string.Equals(i.Category, systemName, StringComparison.OrdinalIgnoreCase))
.ThenBy(i => i.Category)
.ThenBy(i => i.Name);
}
private void Update(NotificationTypeInfo note)
{
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
if (note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("User");
}
else if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("Plugin");
}
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("User");
}
else
{
note.Category = _localization.GetLocalizedString("System");
}
}
}
}

View File

@ -1,35 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{2E030C33-6923-4530-9E54-FA29FA6AD1A9}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -1,23 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Notifications;
namespace Emby.Notifications
{
public class NotificationConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
Key = "notifications",
ConfigurationType = typeof(NotificationOptions)
}
};
}
}
}

View File

@ -1,314 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Notifications;
using Microsoft.Extensions.Logging;
namespace Emby.Notifications
{
/// <summary>
/// Creates notifications for various system events.
/// </summary>
public class NotificationEntryPoint : IServerEntryPoint
{
private readonly ILogger<NotificationEntryPoint> _logger;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
private readonly INotificationManager _notificationManager;
private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationHost _appHost;
private readonly IConfigurationManager _config;
private readonly object _libraryChangedSyncLock = new object();
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private Timer? _libraryUpdateTimer;
private string[] _coreNotificationTypes;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="activityManager">The activity manager.</param>
/// <param name="localization">The localization manager.</param>
/// <param name="notificationManager">The notification manager.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param>
public NotificationEntryPoint(
ILogger<NotificationEntryPoint> logger,
IActivityManager activityManager,
ILocalizationManager localization,
INotificationManager notificationManager,
ILibraryManager libraryManager,
IServerApplicationHost appHost,
IConfigurationManager config)
{
_logger = logger;
_activityManager = activityManager;
_localization = localization;
_notificationManager = notificationManager;
_libraryManager = libraryManager;
_appHost = appHost;
_config = config;
_coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
}
/// <inheritdoc />
public Task RunAsync()
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
}
private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
{
var type = NotificationType.ServerRestartRequired.ToString();
var notification = new NotificationRequest
{
NotificationType = type,
Name = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"),
_appHost.Name)
};
await SendNotification(notification, null).ConfigureAwait(false);
}
private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
var entry = e.Argument;
var type = entry.Type;
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
{
return;
}
var userId = e.Argument.UserId;
if (!userId.Equals(default) && !GetOptions().IsEnabledToMonitorUser(type, userId))
{
return;
}
var notification = new NotificationRequest
{
NotificationType = type,
Name = entry.Name,
Description = entry.Overview
};
await SendNotification(notification, null).ConfigureAwait(false);
}
private NotificationOptions GetOptions()
{
return _config.GetConfiguration<NotificationOptions>("notifications");
}
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
return;
}
lock (_libraryChangedSyncLock)
{
if (_libraryUpdateTimer == null)
{
_libraryUpdateTimer = new Timer(
LibraryUpdateTimerCallback,
null,
5000,
Timeout.Infinite);
}
else
{
_libraryUpdateTimer.Change(5000, Timeout.Infinite);
}
_itemsAdded.Add(e.Item);
}
}
private bool FilterItem(BaseItem item)
{
if (item.IsFolder)
{
return false;
}
if (!item.HasPathProtocol)
{
return false;
}
if (item is IItemByName)
{
return false;
}
return item.SourceType == SourceType.Library;
}
private async void LibraryUpdateTimerCallback(object? state)
{
List<BaseItem> items;
lock (_libraryChangedSyncLock)
{
items = _itemsAdded.ToList();
_itemsAdded.Clear();
_libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
_libraryUpdateTimer = null;
}
if (items.Count > 10)
{
items = items.GetRange(0, 10);
}
foreach (var item in items)
{
var notification = new NotificationRequest
{
NotificationType = NotificationType.NewLibraryContent.ToString(),
Name = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"),
GetItemName(item)),
Description = item.Overview
};
await SendNotification(notification, item).ConfigureAwait(false);
}
}
/// <summary>
/// Creates a human readable name for the item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A human readable name for the item.</returns>
public static string GetItemName(BaseItem item)
{
var name = item.Name;
if (item is Episode episode)
{
if (episode.IndexNumber.HasValue)
{
name = string.Format(
CultureInfo.InvariantCulture,
"Ep{0} - {1}",
episode.IndexNumber.Value,
name);
}
if (episode.ParentIndexNumber.HasValue)
{
name = string.Format(
CultureInfo.InvariantCulture,
"S{0}, {1}",
episode.ParentIndexNumber.Value,
name);
}
}
if (item is IHasSeries hasSeries)
{
name = hasSeries.SeriesName + " - " + name;
}
if (item is IHasAlbumArtist hasAlbumArtist)
{
var artists = hasAlbumArtist.AlbumArtists;
if (artists.Count > 0)
{
name = artists[0] + " - " + name;
}
}
else if (item is IHasArtist hasArtist)
{
var artists = hasArtist.Artists;
if (artists.Count > 0)
{
name = artists[0] + " - " + name;
}
}
return name;
}
private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
{
try
{
await _notificationManager.SendNotification(notification, relatedItem, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending notification");
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_libraryUpdateTimer?.Dispose();
}
_libraryUpdateTimer = null;
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;
}
}
}

View File

@ -1,224 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
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.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Notifications;
using Microsoft.Extensions.Logging;
namespace Emby.Notifications
{
/// <summary>
/// NotificationManager class.
/// </summary>
public class NotificationManager : INotificationManager
{
private readonly ILogger<NotificationManager> _logger;
private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private INotificationService[] _services = Array.Empty<INotificationService>();
private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>();
/// <summary>
/// Initializes a new instance of the <see cref="NotificationManager" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="config">The server configuration manager.</param>
public NotificationManager(
ILogger<NotificationManager> logger,
IUserManager userManager,
IServerConfigurationManager config)
{
_logger = logger;
_userManager = userManager;
_config = config;
}
private NotificationOptions GetConfiguration()
{
return _config.GetConfiguration<NotificationOptions>("notifications");
}
/// <inheritdoc />
public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
{
return SendNotification(request, null, cancellationToken);
}
/// <inheritdoc />
public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken)
{
var notificationType = request.NotificationType;
var options = string.IsNullOrEmpty(notificationType) ?
null :
GetConfiguration().GetOptions(notificationType);
var users = GetUserIds(request, options)
.Select(i => _userManager.GetUserById(i))
.Where(i => relatedItem == null || relatedItem.IsVisibleStandalone(i))
.ToArray();
var title = request.Name;
var description = request.Description;
var tasks = _services.Where(i => IsEnabled(i, notificationType))
.Select(i => SendNotification(request, i, users, title, description, cancellationToken));
return Task.WhenAll(tasks);
}
private Task SendNotification(
NotificationRequest request,
INotificationService service,
IEnumerable<User> users,
string title,
string description,
CancellationToken cancellationToken)
{
users = users.Where(i => IsEnabledForUser(service, i));
var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken));
return Task.WhenAll(tasks);
}
private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
{
if (request.SendToUserMode.HasValue)
{
switch (request.SendToUserMode.Value)
{
case SendToUserType.Admins:
return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
.Select(i => i.Id);
case SendToUserType.All:
return _userManager.UsersIds;
case SendToUserType.Custom:
return request.UserIds;
default:
throw new ArgumentException("Unrecognized SendToUserMode: " + request.SendToUserMode.Value);
}
}
if (options != null && !string.IsNullOrEmpty(request.NotificationType))
{
var config = GetConfiguration();
return _userManager.Users
.Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
.Select(i => i.Id);
}
return request.UserIds;
}
private async Task SendNotification(
NotificationRequest request,
INotificationService service,
string title,
string description,
User user,
CancellationToken cancellationToken)
{
var notification = new UserNotification
{
Date = request.Date,
Description = description,
Level = request.Level,
Name = title,
Url = request.Url,
User = user
};
_logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
try
{
await service.SendNotification(notification, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending notification to {0}", service.Name);
}
}
private bool IsEnabledForUser(INotificationService service, User user)
{
try
{
return service.IsEnabledForUser(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in IsEnabledForUser");
return false;
}
}
private bool IsEnabled(INotificationService service, string notificationType)
{
if (string.IsNullOrEmpty(notificationType))
{
return true;
}
return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
}
/// <inheritdoc />
public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
{
_services = services.ToArray();
_typeFactories = notificationTypeFactories.ToArray();
}
/// <inheritdoc />
public List<NotificationTypeInfo> GetNotificationTypes()
{
var list = _typeFactories.Select(i =>
{
try
{
return i.GetNotificationTypes().ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetNotificationTypes");
return new List<NotificationTypeInfo>();
}
}).SelectMany(i => i).ToList();
var config = GetConfiguration();
foreach (var i in list)
{
i.Enabled = config.IsEnabled(i.Type);
}
return list;
}
/// <inheritdoc />
public IEnumerable<NameIdPair> GetNotificationServices()
{
return _services.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
}).OrderBy(i => i.Name);
}
}
}

View File

@ -1,21 +0,0 @@
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Emby.Notifications")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

View File

@ -15,24 +15,24 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="TagLibSharp" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -27,7 +27,7 @@ namespace Emby.Photos
private readonly IImageProcessor _imageProcessor;
// These are causing taglib to hang
private readonly string[] _includeExtensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
private readonly string[] _includeExtensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif" };
/// <summary>
/// Initializes a new instance of the <see cref="PhotoProvider" /> class.
@ -49,7 +49,7 @@ namespace Emby.Photos
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
return file != null && file.LastWriteTimeUtc != item.DateModified;
return file is not null && file.LastWriteTimeUtc != item.DateModified;
}
return false;
@ -70,20 +70,20 @@ namespace Emby.Photos
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
{
var structure = tag.Structure;
if (structure != null
if (structure is not null
&& structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{
var exifStructure = exif.Structure;
if (exifStructure != null)
if (exifStructure is not null)
{
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
if (entry != null)
if (entry is not null)
{
item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
}
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
if (entry != null)
if (entry is not null)
{
item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
}

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
/// <summary>
/// The _configuration loaded.
/// </summary>
private bool _configurationLoaded;
/// <summary>
/// The _configuration.
/// </summary>
private BaseApplicationConfiguration _configuration;
private BaseApplicationConfiguration? _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
/// <summary>
/// Occurs when [configuration updated].
/// </summary>
public event EventHandler<EventArgs> ConfigurationUpdated;
public event EventHandler<EventArgs>? ConfigurationUpdated;
/// <summary>
/// Occurs when [configuration updating].
/// </summary>
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
/// <summary>
/// Occurs when [named configuration updated].
/// </summary>
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
/// <summary>
/// Gets the type of the configuration.
@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
{
get
{
if (_configurationLoaded)
if (_configuration is not null)
{
return _configuration;
}
lock (_configurationSyncLock)
{
if (_configurationLoaded)
if (_configuration is not null)
{
return _configuration;
}
_configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
_configurationLoaded = true;
return _configuration;
return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
}
}
protected set
{
_configuration = value;
_configurationLoaded = value != null;
}
}
@ -144,7 +131,7 @@ namespace Emby.Server.Implementations.AppBase
{
IConfigurationFactory factory = Activator.CreateInstance<T>();
if (_configurationFactories == null)
if (_configurationFactories is null)
{
_configurationFactories = new[] { factory };
}
@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath;
Directory.CreateDirectory(Path.GetDirectoryName(path));
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock)
{
@ -306,7 +293,7 @@ namespace Emby.Server.Implementations.AppBase
configurationManager._configurationStores,
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
if (configurationInfo == null)
if (configurationInfo is null)
{
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
}
@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType)
{
if (!File.Exists(path))
{
return Activator.CreateInstance(configurationType);
}
try
{
return XmlSerializer.DeserializeFromFile(configurationType, path);
if (File.Exists(path))
{
return XmlSerializer.DeserializeFromFile(configurationType, path);
}
}
catch (IOException)
{
return Activator.CreateInstance(configurationType);
}
catch (Exception ex)
catch (Exception ex) when (ex is not IOException)
{
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
return Activator.CreateInstance(configurationType);
}
return Activator.CreateInstance(configurationType)
?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
}
/// <inheritdoc />
@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key);
Directory.CreateDirectory(Path.GetDirectoryName(path));
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock)
{

View File

@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.AppBase
Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
// If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !newBytes.SequenceEqual(buffer))
if (buffer is null || !newBytes.SequenceEqual(buffer))
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));

View File

@ -11,16 +11,13 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Drawing;
using Emby.Naming.Common;
using Emby.Notifications;
using Emby.Photos;
using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections;
@ -45,6 +42,7 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
@ -70,7 +68,6 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins;
@ -117,15 +114,11 @@ namespace Emby.Server.Implementations
/// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{
/// <summary>
/// The environment variable prefixes to log at server startup.
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
/// <summary>
/// The disposable parts.
/// </summary>
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
@ -134,7 +127,6 @@ namespace Emby.Server.Implementations
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
/// <summary>
@ -143,8 +135,6 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
private DeviceId _deviceId;
private bool _disposed = false;
/// <summary>
@ -168,6 +158,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
@ -193,30 +184,11 @@ namespace Emby.Server.Implementations
/// </summary>
private string PublishedServerUrl => _startupConfig[AddressOverrideKey];
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
{
if (!Environment.UserInteractive)
{
return false;
}
if (_startupOptions.IsService)
{
return false;
}
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
&& !_startupOptions.IsService
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
@ -293,15 +265,7 @@ namespace Emby.Server.Implementations
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
public string SystemId
{
get
{
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
}
public string SystemId => _deviceId.Value;
/// <inheritdoc/>
public string Name => ApplicationProductName;
@ -311,7 +275,7 @@ namespace Emby.Server.Implementations
public X509Certificate2 Certificate { get; private set; }
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
public bool ListenWithHttps => Certificate is not null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
public string FriendlyName =>
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
@ -403,7 +367,7 @@ namespace Emby.Server.Implementations
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(CreateInstanceSafe)
.Where(i => i != null)
.Where(i => i is not null)
.Cast<T>()
.ToList();
@ -424,7 +388,7 @@ namespace Emby.Server.Implementations
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(i => defaultFunc(i))
.Where(i => i != null)
.Where(i => i is not null)
.Cast<T>()
.ToList();
@ -454,7 +418,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath();
Resolve<IMediaEncoder>().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
@ -624,8 +588,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
@ -654,7 +616,7 @@ namespace Emby.Server.Implementations
/// <returns>A task representing the service initialization operation.</returns>
public async Task InitializeServices()
{
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (jellyfinDb.ConfigureAwait(false))
{
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
@ -665,50 +627,19 @@ namespace Emby.Server.Implementations
}
}
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
FindParts();
}
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
{
// Distinct these to prevent users from reporting problems that aren't actually problems
var commandLineArgs = Environment
.GetCommandLineArgs()
.Distinct();
// Get all relevant environment variables
var allEnvVars = Environment.GetEnvironmentVariables();
var relevantEnvVars = new Dictionary<object, object>();
foreach (var key in allEnvVars.Keys)
{
if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
{
relevantEnvVars.Add(key, allEnvVars[key]);
}
}
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
logger.LogInformation("Arguments: {Args}", commandLineArgs);
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
private X509Certificate2 GetCertificate(string path, string password)
{
if (string.IsNullOrWhiteSpace(path))
@ -795,13 +726,7 @@ namespace Emby.Server.Implementations
Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
}
/// <summary>
@ -935,17 +860,13 @@ namespace Emby.Server.Implementations
/// </summary>
public void Restart()
{
if (!CanSelfRestart)
{
throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually.");
}
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
_pluginManager.UnloadAssemblies();
Task.Run(async () =>
{
@ -1004,9 +925,6 @@ namespace Emby.Server.Implementations
// Local metadata
yield return typeof(BoxSetXmlSaver).Assembly;
// Notifications
yield return typeof(NotificationManager).Assembly;
// Xbmc
yield return typeof(ArtistNfoProvider).Assembly;
@ -1045,15 +963,11 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
};
}
@ -1065,7 +979,6 @@ namespace Emby.Server.Implementations
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
@ -1275,10 +1188,13 @@ namespace Emby.Server.Implementations
}
}
// used for closing websockets
foreach (var session in _sessionManager.Sessions)
if (_sessionManager != null)
{
await session.DisposeAsync().ConfigureAwait(false);
// used for closing websockets
foreach (var session in _sessionManager.Sessions)
{
await session.DisposeAsync().ConfigureAwait(false);
}
}
}
}

View File

@ -66,6 +66,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param>
/// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="channels">The channels.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem,
IUserDataManager userDataManager,
IProviderManager providerManager,
IMemoryCache memoryCache)
IMemoryCache memoryCache,
IEnumerable<IChannel> channels)
{
_userManager = userManager;
_dtoService = dtoService;
@ -86,18 +88,13 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager;
_providerManager = providerManager;
_memoryCache = memoryCache;
}
internal IChannel[] Channels { get; private set; }
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
/// <inheritdoc />
public void AddParts(IEnumerable<IChannel> channels)
{
Channels = channels.ToArray();
}
internal IChannel[] Channels { get; }
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
/// <inheritdoc />
public bool EnableMediaSourceDisplay(BaseItem item)
{
@ -129,7 +126,7 @@ namespace Emby.Server.Implementations.Channels
public Task DeleteItem(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
if (internalChannel == null)
if (internalChannel is null)
{
throw new ArgumentException(nameof(item.ChannelId));
}
@ -160,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
var channels = GetAllChannels()
.Select(GetChannelEntity)
var channels = await GetAllChannelEntitiesAsync()
.OrderBy(i => i.SortName)
.ToList();
.ToListAsync()
.ConfigureAwait(false);
if (query.IsRecordingsFolder.HasValue)
{
@ -227,8 +224,9 @@ namespace Emby.Server.Implementations.Channels
.ToList();
}
if (user != null)
if (user is not null)
{
var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
channels = channels.Where(i =>
{
if (!i.IsVisible(user))
@ -238,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
try
{
return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
return GetChannelProvider(i).IsEnabledFor(userId);
}
catch
{
@ -253,7 +251,7 @@ namespace Emby.Server.Implementations.Channels
if (query.StartIndex.HasValue || query.Limit.HasValue)
{
int startIndex = query.StartIndex ?? 0;
int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
all = all.GetRange(startIndex, count);
}
@ -261,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in all)
{
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
}
}
@ -272,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
var internalResult = GetChannelsInternal(query);
var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
var dtoOptions = new DtoOptions();
@ -330,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
progress.Report(100);
}
private Channel GetChannelEntity(IChannel channel)
private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
{
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
foreach (IChannel channel in GetAllChannels())
{
yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
}
}
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
@ -355,7 +356,7 @@ namespace Emby.Server.Implementations.Channels
{
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
if (mediaSources == null || mediaSources.Count == 0)
if (mediaSources is null || mediaSources.Count == 0)
{
try
{
@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
}
else
{
results = new List<MediaSourceInfo>();
results = Enumerable.Empty<MediaSourceInfo>();
}
return results
@ -447,7 +448,7 @@ namespace Emby.Server.Implementations.Channels
var item = _libraryManager.GetItemById(id) as Channel;
if (item == null)
if (item is null)
{
item = new Channel
{
@ -601,10 +602,7 @@ namespace Emby.Server.Implementations.Channels
private Guid GetInternalChannelId(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
ArgumentException.ThrowIfNullOrEmpty(name);
return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
}
@ -739,7 +737,7 @@ namespace Emby.Server.Implementations.Channels
query.GroupByPresentationUniqueKey = false;
// null if came from cache
if (itemsResult != null)
if (itemsResult is not null)
{
var items = itemsResult.Items;
var itemsLen = items.Count;
@ -761,7 +759,7 @@ namespace Emby.Server.Implementations.Channels
foreach (var deadId in deadIds)
{
var deadItem = _libraryManager.GetItemById(deadId);
if (deadItem != null)
if (deadItem is not null)
{
_libraryManager.DeleteItem(
deadItem,
@ -813,7 +811,7 @@ namespace Emby.Server.Implementations.Channels
{
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
if (cachedResult is not null)
{
return null;
}
@ -836,7 +834,7 @@ namespace Emby.Server.Implementations.Channels
{
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
if (cachedResult is not null)
{
return null;
}
@ -861,7 +859,7 @@ namespace Emby.Server.Implementations.Channels
var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false);
if (result == null)
if (result is null)
{
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
}
@ -955,7 +953,7 @@ namespace Emby.Server.Implementations.Channels
_logger.LogError(ex, "Error retrieving channel item from database");
}
if (item == null)
if (item is null)
{
item = new T();
isNew = true;
@ -1156,7 +1154,7 @@ namespace Emby.Server.Implementations.Channels
{
_libraryManager.CreateItem(item, parentFolder);
if (info.People != null && info.People.Count > 0)
if (info.People is not null && info.People.Count > 0)
{
_libraryManager.UpdatePeople(item, info.People);
}
@ -1193,7 +1191,7 @@ namespace Emby.Server.Implementations.Channels
var result = GetAllChannels()
.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase));
if (result == null)
if (result is null)
{
throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name);
}
@ -1206,7 +1204,7 @@ namespace Emby.Server.Implementations.Channels
var result = GetAllChannels()
.FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name)));
if (result == null)
if (result is null)
{
throw new ResourceNotFoundException("No channel provider found for channel id " + internalChannelId);
}

View File

@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Collections
var episode = subItem as Episode;
var series = episode?.Series;
if (series != null && series.HasImage(ImageType.Primary))
if (series is not null && series.HasImage(ImageType.Primary))
{
return series;
}
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Collections
var parent = subItem.GetOwner() ?? subItem.GetParent();
if (parent != null && parent.HasImage(ImageType.Primary))
if (parent is not null && parent.HasImage(ImageType.Primary))
{
if (parent is MusicAlbum)
{
@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Collections
return null;
})
.Where(i => i != null)
.Where(i => i is not null)
.GroupBy(x => x!.Id) // We removed the null values
.Select(x => x.First())
.ToList()!; // Again... the list doesn't contain any null values

View File

@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Collections
internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
{
var existingFolder = FindFolders(path).FirstOrDefault();
if (existingFolder != null)
if (existingFolder is not null)
{
return existingFolder;
}
@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Collections
{
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null
return folder is null
? Enumerable.Empty<BoxSet>()
: folder.GetChildren(user, true).OfType<BoxSet>();
}
@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Collections
var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
if (parentFolder == null)
if (parentFolder is null)
{
throw new ArgumentException(nameof(parentFolder));
}
@ -206,8 +206,7 @@ namespace Emby.Server.Implementations.Collections
throw new ArgumentException("No collection exists with the supplied Id");
}
var list = new List<LinkedChild>();
var itemList = new List<BaseItem>();
List<BaseItem>? itemList = null;
var linkedChildrenList = collection.GetLinkedChildren();
var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
@ -216,25 +215,30 @@ namespace Emby.Server.Implementations.Collections
{
var item = _libraryManager.GetItemById(id);
if (item == null)
if (item is null)
{
throw new ArgumentException("No item exists with the supplied Id");
}
if (!currentLinkedChildrenIds.Contains(id))
{
itemList.Add(item);
(itemList ??= new()).Add(item);
list.Add(LinkedChild.Create(item));
linkedChildrenList.Add(item);
}
}
if (list.Count > 0)
if (itemList is not null)
{
LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
var originalLen = collection.LinkedChildren.Length;
var newItemCount = itemList.Count;
LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
collection.LinkedChildren.CopyTo(newChildren, 0);
list.CopyTo(newChildren, collection.LinkedChildren.Length);
for (int i = 0; i < newItemCount; i++)
{
newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
}
collection.LinkedChildren = newChildren;
collection.UpdateRatingToItems(linkedChildrenList);
@ -265,9 +269,9 @@ namespace Emby.Server.Implementations.Collections
{
var childItem = _libraryManager.GetItemById(guidId);
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
if (child == null)
if (child is null)
{
_logger.LogWarning("No collection title exists with the supplied Id");
continue;
@ -275,7 +279,7 @@ namespace Emby.Server.Implementations.Collections
list.Add(child);
if (childItem != null)
if (childItem is not null)
{
itemList.Add(childItem);
}

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Globalization;
using System.IO;
@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
/// <summary>
/// Configuration updating event.
/// </summary>
public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating;
public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
/// <summary>
/// Gets the type of the configuration.

View File

@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets a new copy of the default configuration options.
/// </summary>
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
public static Dictionary<string, string?> DefaultConfiguration => new()
{
{ HostWebClientKey, bool.TrueString },
{ DefaultRedirectKey, "web/index.html" },
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString }
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" }
};
}
}

View File

@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography;
using static MediaBrowser.Model.Cryptography.Constants;
@ -14,25 +12,6 @@ namespace Emby.Server.Implementations.Cryptography
/// </summary>
public class CryptographyProvider : ICryptoProvider
{
// TODO: remove when not needed for backwards compat
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
"System.Security.Cryptography.MD5",
"SHA",
"SHA1",
"System.Security.Cryptography.SHA1",
"SHA256",
"SHA-256",
"System.Security.Cryptography.SHA256",
"SHA384",
"SHA-384",
"System.Security.Cryptography.SHA384",
"SHA512",
"SHA-512",
"System.Security.Cryptography.SHA512"
};
/// <inheritdoc />
public string DefaultHashMethod => "PBKDF2-SHA512";
@ -80,22 +59,7 @@ namespace Emby.Server.Implementations.Cryptography
DefaultOutputLength));
}
if (!_supportedHashMethods.Contains(hash.Id))
{
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
}
using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
if (hash.Salt.Length == 0)
{
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
}
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
Array.Copy(bytes, salted, bytes.Length);
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
}
/// <inheritdoc />

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets or sets the path to the DB file.
/// </summary>
/// <value>Path to the DB file.</value>
protected string DbFilePath { get; set; }
/// <summary>
/// Gets or sets the number of write connections to create.
/// </summary>
/// <value>Path to the DB file.</value>
protected int WriteConnectionsCount { get; set; } = 1;
/// <summary>
/// Gets or sets the number of read connections to create.
/// </summary>
protected int ReadConnectionsCount { get; set; } = 1;
/// <summary>
/// Gets the logger.
/// </summary>
@ -60,11 +69,23 @@ namespace Emby.Server.Implementations.Data
/// <value>The cache size or null.</value>
protected virtual int? CacheSize => null;
/// <summary>
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
/// </summary>
protected virtual string LockingMode => "NORMAL";
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode => "TRUNCATE";
protected virtual string JournalMode => "WAL";
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
/// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
/// <summary>
/// Gets the page size.
@ -77,66 +98,119 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore => TempStoreMode.Default;
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
/// <summary>
/// Gets the synchronous mode.
/// </summary>
/// <value>The synchronous mode or null.</value>
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => null;
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
/// <summary>
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
protected ConnectionPool WriteConnections { get; set; }
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
protected ConnectionPool ReadConnections { get; set; }
public virtual void Initialize()
{
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
// Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection())
{
connection.Execute("VACUUM");
}
}
protected ManagedConnection GetConnection(bool readOnly = false)
{
WriteLock.Wait();
if (WriteConnection != null)
{
return new ManagedConnection(WriteConnection, WriteLock);
}
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
WriteConnection = SQLite3.Open(
protected SQLiteDatabaseConnection CreateWriteConnection()
{
var writeConnection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
if (CacheSize.HasValue)
{
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
// Configuration and pragmas can affect VACUUM so it needs to be last.
WriteConnection.Execute("VACUUM");
return writeConnection;
}
return new ManagedConnection(WriteConnection, WriteLock);
protected SQLiteDatabaseConnection CreateReadConnection()
{
var connection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
null);
if (CacheSize.HasValue)
{
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
connection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
connection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
}
public IStatement PrepareStatement(ManagedConnection connection, string sql)
@ -145,18 +219,6 @@ namespace Emby.Server.Implementations.Data
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
{
int len = sql.Count;
IStatement[] statements = new IStatement[len];
for (int i = 0; i < len; i++)
{
statements[i] = connection.PrepareStatement(sql[i]);
}
return statements;
}
protected bool TableExists(ManagedConnection connection, string name)
{
return connection.RunInTransaction(
@ -231,22 +293,10 @@ namespace Emby.Server.Implementations.Data
if (dispose)
{
WriteLock.Wait();
try
{
WriteConnection?.Dispose();
}
finally
{
WriteLock.Release();
}
WriteLock.Dispose();
WriteConnections.Dispose();
ReadConnections.Dispose();
}
WriteConnection = null;
WriteLock = null;
_disposed = true;
}
}

View File

@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data
var item = _libraryManager.GetItemById(itemId);
if (item != null)
if (item is not null)
{
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Concurrent;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data;
/// <summary>
/// A pool of SQLite Database connections.
/// </summary>
public sealed class ConnectionPool : IDisposable
{
private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionPool" /> class.
/// </summary>
/// <param name="count">The number of database connection to create.</param>
/// <param name="factory">Factory function to create the database connections.</param>
public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
{
for (int i = 0; i < count; i++)
{
_connections.Add(factory.Invoke());
}
}
/// <summary>
/// Gets a database connection from the pool if one is available, otherwise blocks.
/// </summary>
/// <returns>A database connection.</returns>
public ManagedConnection GetConnection()
{
if (_disposed)
{
ThrowObjectDisposedException();
}
return new ManagedConnection(_connections.Take(), this);
static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(ConnectionPool));
}
}
/// <summary>
/// Return a database connection to the pool.
/// </summary>
/// <param name="connection">The database connection to return.</param>
public void Return(SQLiteDatabaseConnection connection)
{
if (_disposed)
{
connection.Dispose();
return;
}
_connections.Add(connection);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var connection in _connections)
{
connection.Dispose();
}
_connections.Dispose();
_disposed = true;
}
}

View File

@ -2,23 +2,22 @@
using System;
using System.Collections.Generic;
using System.Threading;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public sealed class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim _writeLock;
private readonly ConnectionPool _pool;
private SQLiteDatabaseConnection? _db;
private SQLiteDatabaseConnection _db;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
{
_db = db;
_writeLock = writeLock;
_pool = pool;
}
public IStatement PrepareStatement(string sql)
@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data
return;
}
_writeLock.Release();
_pool.Return(_db);
_db = null; // Don't dispose it
_db = null!; // Don't dispose it
_disposed = true;
}
}

View File

@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.Data
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
if (value == null)
if (value is null)
{
bindParam.BindNull();
}

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data
{
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
{
private readonly IUserManager _userManager;
public SqliteUserDataRepository(
ILogger<SqliteUserDataRepository> logger,
IApplicationPaths appPaths)
IServerConfigurationManager config,
IUserManager userManager)
: base(logger)
{
DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
_userManager = userManager;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <param name="userManager">The user manager.</param>
/// <param name="dbLock">The lock to use for database IO.</param>
/// <param name="dbConnection">The connection to use for database IO.</param>
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
public override void Initialize()
{
WriteLock.Dispose();
WriteLock = dbLock;
WriteConnection?.Dispose();
WriteConnection = dbConnection;
base.Initialize();
using (var connection = GetConnection())
{
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : userManager.Users;
var users = userDatasTableExists ? null : _userManager.Users;
connection.RunInTransaction(
db =>
@ -140,10 +139,7 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException(nameof(key));
}
ArgumentException.ThrowIfNullOrEmpty(key);
PersistUserData(userId, key, userData, cancellationToken);
}
@ -274,10 +270,7 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException(nameof(key));
}
ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection(true))
{
@ -377,20 +370,5 @@ namespace Emby.Server.Implementations.Data
return userData;
}
#pragma warning disable CA2215
/// <inheritdoc/>
/// <remarks>
/// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
/// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
/// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
/// </remarks>
protected override void Dispose(bool dispose)
{
// The write lock and connection for the item repository are shared with the user data repository
// since they point to the same database. The item repo has responsibility for disposing these two objects,
// so the user data repo should not attempt to dispose them as well
}
#pragma warning restore CA2215
}
}

View File

@ -23,14 +23,11 @@ namespace Emby.Server.Implementations.Data
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
public Type? GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
throw new ArgumentNullException(nameof(typeName));
}
ArgumentException.ThrowIfNullOrEmpty(typeName);
return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(k))
.FirstOrDefault(t => t != null));
.FirstOrDefault(t => t is not null));
}
}
}

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@ -83,22 +82,23 @@ namespace Emby.Server.Implementations.Dto
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
var returnItems = new BaseItemDto[items.Count];
var programTuples = new List<(BaseItem, BaseItemDto)>();
var channelTuples = new List<(BaseItemDto, LiveTvChannel)>();
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var returnItems = new BaseItemDto[accessibleItems.Count];
List<(BaseItem, BaseItemDto)> programTuples = null;
List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
for (int index = 0; index < items.Count; index++)
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = items[index];
var item = accessibleItems[index];
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
{
channelTuples.Add((dto, tvChannel));
(channelTuples ??= new()).Add((dto, tvChannel));
}
else if (item is LiveTvProgram)
{
programTuples.Add((item, dto));
(programTuples ??= new()).Add((item, dto));
}
if (item is IItemByName byName)
@ -121,12 +121,12 @@ namespace Emby.Server.Implementations.Dto
returnItems[index] = dto;
}
if (programTuples.Count > 0)
if (programTuples is not null)
{
LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
}
if (channelTuples.Count > 0)
if (channelTuples is not null)
{
LivetvManager.AddChannelInfo(channelTuples, options, user);
}
@ -213,7 +213,7 @@ namespace Emby.Server.Implementations.Dto
dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture);
}
if (user != null)
if (user is not null)
{
AttachUserSpecificInfo(dto, item, user, options);
}
@ -235,14 +235,14 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.CanDelete))
{
dto.CanDelete = user == null
dto.CanDelete = user is null
? item.CanDelete()
: item.CanDelete(user);
}
if (options.ContainsField(ItemFields.CanDownload))
{
dto.CanDownload = user == null
dto.CanDownload = user is null
? item.CanDownload()
: item.CanDownload(user);
}
@ -254,7 +254,7 @@ namespace Emby.Server.Implementations.Dto
var liveTvManager = LivetvManager;
var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
if (activeRecording != null)
if (activeRecording is not null)
{
dto.Type = BaseItemKind.Recording;
dto.CanDownload = false;
@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Dto
{
var dto = GetBaseItemDtoInternal(item, options, user);
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts))
{
SetItemByNameInfo(item, dto, taggedItems);
}
@ -417,7 +417,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.BasicSyncInfo))
{
var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading);
var userCanSync = user is not null && user.HasPermission(PermissionKind.EnableContentDownloading);
if (userCanSync && item.SupportsExternalTransfer)
{
dto.SupportsSync = true;
@ -460,7 +460,7 @@ namespace Emby.Server.Implementations.Dto
var album = item.AlbumEntity;
if (album != null)
if (album is not null)
{
dto.Album = album.Name;
dto.AlbumId = album.Id;
@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.Dto
{
return images
.Select(p => GetImageCacheTag(item, p))
.Where(i => i != null)
.Where(i => i is not null)
.ToArray();
}
@ -522,32 +522,32 @@ namespace Emby.Server.Implementations.Dto
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i =>
{
if (i.IsType(PersonType.Actor))
if (i.IsType(PersonKind.Actor))
{
return 0;
}
if (i.IsType(PersonType.GuestStar))
if (i.IsType(PersonKind.GuestStar))
{
return 1;
}
if (i.IsType(PersonType.Director))
if (i.IsType(PersonKind.Director))
{
return 2;
}
if (i.IsType(PersonType.Writer))
if (i.IsType(PersonKind.Writer))
{
return 3;
}
if (i.IsType(PersonType.Producer))
if (i.IsType(PersonKind.Producer))
{
return 4;
}
if (i.IsType(PersonType.Composer))
if (i.IsType(PersonKind.Composer))
{
return 4;
}
@ -570,12 +570,9 @@ namespace Emby.Server.Implementations.Dto
_logger.LogError(ex, "Error getting person {Name}", c);
return null;
}
}).Where(i => i != null)
.Where(i => user == null ?
true :
i.IsVisible(user))
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
}).Where(i => i is not null)
.Where(i => user is null || i.IsVisible(user))
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < people.Count; i++)
@ -593,13 +590,13 @@ namespace Emby.Server.Implementations.Dto
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id;
if (dto.ImageBlurHashes != null)
if (dto.ImageBlurHashes is not null)
{
// Only add BlurHash for the person's image.
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
if (blurHash != null)
if (blurHash is not null)
{
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
foreach (var (imageId, blurHashValue) in blurHash)
@ -662,7 +659,7 @@ namespace Emby.Server.Implementations.Dto
private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
{
var image = item.GetImageInfo(imageType, imageIndex);
if (image != null)
if (image is not null)
{
return GetTagAndFillBlurhash(dto, item, image);
}
@ -782,7 +779,7 @@ namespace Emby.Server.Implementations.Dto
{
var tag = GetTagAndFillBlurhash(dto, item, image);
if (tag != null)
if (tag is not null)
{
dto.ImageTags[image.Type] = tag;
}
@ -909,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
// Add audio info
if (item is Audio audio)
{
dto.LUFS = audio.LUFS;
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
{
@ -917,7 +915,7 @@ namespace Emby.Server.Implementations.Dto
var albumParent = audio.AlbumEntity;
if (albumParent != null)
if (albumParent is not null)
{
dto.AlbumId = albumParent.Id;
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
@ -967,7 +965,7 @@ namespace Emby.Server.Implementations.Dto
{
EnableImages = false
});
if (artist != null)
if (artist is not null)
{
return new NameGuidPair
{
@ -977,7 +975,7 @@ namespace Emby.Server.Implementations.Dto
}
return null;
}).Where(i => i != null).ToArray();
}).Where(i => i is not null).ToArray();
}
if (item is IHasAlbumArtist hasAlbumArtist)
@ -1016,7 +1014,7 @@ namespace Emby.Server.Implementations.Dto
{
EnableImages = false
});
if (artist != null)
if (artist is not null)
{
return new NameGuidPair
{
@ -1026,7 +1024,7 @@ namespace Emby.Server.Implementations.Dto
}
return null;
}).Where(i => i != null).ToArray();
}).Where(i => i is not null).ToArray();
}
// Add video info
@ -1073,7 +1071,7 @@ namespace Emby.Server.Implementations.Dto
{
MediaStream[] mediaStreams;
if (dto.MediaSources != null && dto.MediaSources.Length > 0)
if (dto.MediaSources is not null && dto.MediaSources.Length > 0)
{
if (item.SourceType == SourceType.Channel)
{
@ -1140,10 +1138,10 @@ namespace Emby.Server.Implementations.Dto
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
episodeSeries ??= episode.Series;
if (episodeSeries != null)
if (episodeSeries is not null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
@ -1153,7 +1151,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
episodeSeries ??= episode.Series;
if (episodeSeries != null)
if (episodeSeries is not null)
{
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
}
@ -1179,7 +1177,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
series ??= season.Series;
if (series != null)
if (series is not null)
{
dto.SeriesStudio = series.Studios.FirstOrDefault();
}
@ -1190,10 +1188,10 @@ namespace Emby.Server.Implementations.Dto
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
series ??= season.Series;
if (series != null)
if (series is not null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
if (dto.ImageTags is null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, series);
}
@ -1256,7 +1254,7 @@ namespace Emby.Server.Implementations.Dto
if (item.SourceType == SourceType.Channel)
{
var channel = _libraryManager.GetItemById(item.ChannelId);
if (channel != null)
if (channel is not null)
{
dto.ChannelName = channel.Name;
}
@ -1268,7 +1266,7 @@ namespace Emby.Server.Implementations.Dto
if (currentItem is MusicAlbum musicAlbum)
{
var artist = musicAlbum.GetMusicArtist(new DtoOptions(false));
if (artist != null)
if (artist is not null)
{
return artist;
}
@ -1276,7 +1274,7 @@ namespace Emby.Server.Implementations.Dto
var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
if (parent is null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
{
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
}
@ -1309,53 +1307,53 @@ namespace Emby.Server.Implementations.Dto
var imageTags = dto.ImageTags;
while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
while ((!(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
|| (!(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|| (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|| parent is Series)
{
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
if (parent == null)
if (parent is null)
{
break;
}
var allImages = parent.ImageInfos;
if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null)
if (logoLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null)
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
if (image != null)
if (image is not null)
{
dto.ParentLogoItemId = parent.Id;
dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
}
}
if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null)
if (artLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null)
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
if (image != null)
if (image is not null)
{
dto.ParentArtItemId = parent.Id;
dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
}
}
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
if (thumbLimit > 0 && !(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
if (image != null)
if (image is not null)
{
dto.ParentThumbItemId = parent.Id;
dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
}
}
if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0)))
if (backdropLimit > 0 && !((dto.BackdropImageTags is not null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags is not null && dto.ParentBackdropImageTags.Length > 0)))
{
var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
@ -1403,7 +1401,7 @@ namespace Emby.Server.Implementations.Dto
{
var imageInfo = item.GetImageInfo(ImageType.Primary, 0);
if (imageInfo == null)
if (imageInfo is null)
{
return null;
}

View File

@ -7,7 +7,6 @@
<ItemGroup>
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
@ -18,22 +17,22 @@
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" />
<PackageReference Include="Mono.Nat" Version="3.0.4" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.3.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" />
<PackageReference Include="SQLitePCL.pretty.netstandard" />
<PackageReference Include="DotNet.Glob" />
</ItemGroup>
<ItemGroup>
@ -41,7 +40,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
@ -49,18 +48,18 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_libraryChangedSyncLock)
{
if (LibraryUpdateTimer == null)
if (LibraryUpdateTimer is null)
{
LibraryUpdateTimer = new Timer(
LibraryUpdateTimerCallback,
@ -227,7 +227,7 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_libraryChangedSyncLock)
{
if (LibraryUpdateTimer == null)
if (LibraryUpdateTimer is null)
{
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
@ -254,7 +254,7 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_libraryChangedSyncLock)
{
if (LibraryUpdateTimer == null)
if (LibraryUpdateTimer is null)
{
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
@ -276,30 +276,33 @@ namespace Emby.Server.Implementations.EntryPoints
/// Libraries the update timer callback.
/// </summary>
/// <param name="state">The state.</param>
private void LibraryUpdateTimerCallback(object state)
private async void LibraryUpdateTimerCallback(object state)
{
List<Folder> foldersAddedTo;
List<Folder> foldersRemovedFrom;
List<BaseItem> itemsUpdated;
List<BaseItem> itemsAdded;
List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
var foldersAddedTo = _foldersAddedTo
.GroupBy(x => x.Id)
.Select(x => x.First())
foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
var foldersRemovedFrom = _foldersRemovedFrom
.GroupBy(x => x.Id)
.Select(x => x.First())
foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
var itemsUpdated = _itemsUpdated
itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.GroupBy(x => x.Id)
.Select(x => x.First())
.DistinctBy(x => x.Id)
.ToList();
SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
itemsAdded = _itemsAdded.ToList();
itemsRemoved = _itemsRemoved.ToList();
if (LibraryUpdateTimer != null)
if (LibraryUpdateTimer is not null)
{
LibraryUpdateTimer.Dispose();
LibraryUpdateTimer = null;
@ -311,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear();
_foldersRemovedFrom.Clear();
}
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@ -475,7 +480,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (dispose)
{
if (LibraryUpdateTimer != null)
if (LibraryUpdateTimer is not null)
{
LibraryUpdateTimer.Dispose();
LibraryUpdateTimer = null;

View File

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_syncLock)
{
if (_updateTimer == null)
if (_updateTimer is null)
{
_updateTimer = new Timer(
UpdateTimerCallback,
@ -75,11 +75,11 @@ namespace Emby.Server.Implementations.EntryPoints
var baseItem = e.Item;
// Go up one level for indicators
if (baseItem != null)
if (baseItem is not null)
{
var parent = baseItem.GetOwner() ?? baseItem.GetParent();
if (parent != null)
if (parent is not null)
{
keys.Add(parent);
}
@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
private void UpdateTimerCallback(object? state)
private async void UpdateTimerCallback(object? state)
{
List<KeyValuePair<Guid, List<BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
var changes = _changedItems.ToList();
changes = _changedItems.ToList();
_changedItems.Clear();
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
if (_updateTimer != null)
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;
}
}
await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
}
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
{
foreach (var pair in changes)
foreach ((var key, var value) in changes)
{
await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
}
}
@ -123,8 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints
var user = _userManager.GetUserById(userId);
var dtoList = changedItems
.GroupBy(x => x.Id)
.Select(x => x.First())
.DistinctBy(x => x.Id)
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
@ -145,7 +145,7 @@ namespace Emby.Server.Implementations.EntryPoints
public void Dispose()
{
if (_updateTimer != null)
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;

View File

@ -164,7 +164,7 @@ namespace Emby.Server.Implementations.HttpServer
ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
ReadOnlySequence<byte> buffer = result.Buffer;
if (OnReceive == null)
if (OnReceive is null)
{
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
@ -185,7 +185,7 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
if (stub == null)
if (stub is null)
{
_logger.LogError("Error processing web socket message");
return;

Some files were not shown because too many files have changed in this diff Show More