Compare commits

..

4 Commits

Author SHA1 Message Date
Cody Robibero
7852f0b426 Ignore management controller when generating openapi spec 2021-08-18 05:06:17 -06:00
Cody Robibero
1bbe262646 Fix build 2021-08-18 05:05:47 -06:00
Claus Vium
bc69aa251b Merge branch 'master' into EraYaN-add-management-interface 2021-08-18 08:56:14 +02:00
Erwin de Haan
c5d900b164 Add initial management interface support. 2020-10-21 17:28:00 +02:00
912 changed files with 15157 additions and 21678 deletions

View File

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

View File

@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 6.0.x
DotNetSdkVersion: 5.0.302
jobs:
- job: Build
@@ -91,10 +91,3 @@ jobs:
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Extensions'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
artifactName: 'Jellyfin.Extensions'

View File

@@ -39,14 +39,6 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- script: './bump-version $(JellyfinVersion)'
displayName: Bump internal version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
@@ -88,14 +80,6 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- script: './bump-version $(JellyfinVersion)'
displayName: Bump internal version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
@@ -143,10 +127,6 @@ jobs:
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- script: './bump-version $(JellyfinVersion)'
displayName: Bump internal version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -201,7 +181,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
@@ -215,10 +195,10 @@ jobs:
steps:
- task: UseDotNet@2
displayName: 'Use .NET 6.0 sdk'
displayName: 'Use .NET 5.0 sdk'
inputs:
packageType: 'sdk'
version: '6.0.x'
version: '5.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
@@ -231,7 +211,6 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
@@ -246,7 +225,6 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'

View File

@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 6.0.x
default: 5.0.302
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/net5.0/openapi.json"
artifactName: 'OpenAPI Spec'

View File

@@ -5,6 +5,8 @@ variables:
value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
value: 5.0.302
pr:
autoCancel: true
@@ -55,9 +57,6 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
Extensions:
NugetPackageName: Jellyfin.Extensions
AssemblyFileName: Jellyfin.Extensions.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:

1
.copr
View File

@@ -1 +0,0 @@
fedora

1
.copr/Makefile Symbolic link
View File

@@ -0,0 +1 @@
../fedora/Makefile

50
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,50 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**System (please complete the following information):**
- OS: [e.g. Debian, Windows]
- Virtualization: [e.g. Docker, KVM, LXC]
- Clients: [Browser, Android, Fire Stick, etc.]
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
- Base URL: [e.g. none, yes: /example]
- Networking: [e.g. Host, Bridge/NAT]
- Storage: [e.g. local, NFS, cloud]
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Server Logs**
<!-- Please paste any log errors. -->
**FFmpeg Logs**
<!-- Please paste any log errors. -->
**Browser Console Logs**
<!-- Please paste any log errors. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@@ -1,106 +0,0 @@
name: Issue Report
description: File an issue report
title: "[Issue]: "
labels: [bug, triage]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
- type: textarea
id: what-happened
attributes:
label: Please describe your bug
description: Also tell us, what did you expect to happen?
placeholder: |
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
This is my issue.
Steps to Reproduce
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: true
- type: dropdown
id: version
attributes:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- 10.7.7
- 10.7.z
- 10.6.4
- Other
validations:
required: true
- type: input
id: version-other
attributes:
label: "if other:"
placeholder: Other
- type: textarea
attributes:
label: Environment
description: |
Examples:
- **OS**: [e.g. Debian, Windows]
- **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]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT]
- **Storage**: [e.g. local, NFS, cloud]
value: |
- OS:
- Virtualization:
- Clients:
- Browser:
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
- Plugins:
- Reverse Proxy:
- Base URL:
- Networking:
- Storage:
render: markdown
- type: textarea
id: logs
attributes:
label: Jellyfin logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
render: shell
- type: textarea
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.
render: shell
- type: textarea
id: browserlogs
attributes:
label: Please attach any browser or client logs here
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
- type: textarea
id: screenshots
attributes:
label: Please attach any screenshots here
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -24,8 +24,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
dotnet-version: '5.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:

View File

@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.5
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -1,124 +0,0 @@
name: OpenAPI
on:
push:
branches:
- master
pull_request_target:
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.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@v2
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
openapi-base:
name: OpenAPI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.base_ref }}
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.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@v2
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
openapi-diff:
name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
needs:
- openapi-head
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@v2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@v2
with:
name: openapi-base
path: openapi-base
- name: Workaround openapi-diff issue
run: |
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
- name: Calculate OpenAPI difference
uses: docker://openapitools/openapi-diff
continue-on-error: true
with:
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
- id: read-diff
name: Read openapi-diff output
run: |
body=$(cat openapi-changes.md)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
uses: peter-evans/find-comment@v1
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@v1.4.5
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
<details>
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
${{ steps.read-diff.outputs.body }}
</details>
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@v1.4.5
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
No changes to OpenAPI specification found. See history of this comment for previous changes.

3
.gitignore vendored
View File

@@ -278,6 +278,3 @@ web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated
# Omnisharp crash logs
mono_crash.*.json

4
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",

View File

@@ -46,7 +46,6 @@
- [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername)
- [gnattu](https://github.com/gnattu)
- [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
@@ -149,8 +148,6 @@
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
- [MBR-0001](https://github.com/MBR-0001)
- [jonas-resch](https://github.com/jonas-resch)
# Emby Contributors
@@ -215,5 +212,4 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
- [lbenini](https://github.com/lbenini)
- [gnuyent](https://github.com/gnuyent)

View File

@@ -3,11 +3,8 @@
<PropertyGroup>
<Nullable>enable</Nullable>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -1,8 +1,4 @@
# DESIGNED FOR BUILDING ON AMD64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
ARG DOTNET_VERSION=5.0
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@@ -12,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM debian:stable-slim as app
FROM debian:buster-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -22,16 +18,15 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
# https://github.com/intel/compute-runtime/releases
ARG GMMLIB_VERSION=21.2.1
ARG IGC_VERSION=1.0.8517
ARG NEO_VERSION=21.35.20826
ARG LEVEL_ZERO_VERSION=1.2.20826
ARG GMMLIB_VERSION=20.3.2
ARG IGC_VERSION=1.0.5435
ARG NEO_VERSION=20.46.18421
ARG LEVEL_ZERO_VERSION=1.0.18421
# Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
# curl: healthcheck
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
@@ -62,7 +57,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -77,8 +72,6 @@ RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --
FROM app
ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -88,6 +81,3 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1

View File

@@ -1,8 +1,8 @@
# DESIGNED FOR BUILDING ON ARM ONLY
# DESIGNED FOR BUILDING ON AMD64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
ARG DOTNET_VERSION=5.0
FROM node:lts-alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
FROM arm32v7/debian:stable-slim as app
FROM arm32v7/debian:buster-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,8 +24,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
# curl: setup & healthcheck
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
@@ -44,7 +42,7 @@ RUN apt-get update \
vainfo \
libva2 \
locales \
&& apt-get remove gnupg -y \
&& apt-get remove curl gnupg -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -52,7 +50,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -68,8 +66,6 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM app
ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -79,6 +75,3 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1

View File

@@ -1,8 +1,8 @@
# DESIGNED FOR BUILDING ON ARM64 ONLY
# DESIGNED FOR BUILDING ON AMD64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
ARG DOTNET_VERSION=5.0
FROM node:lts-alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:stable-slim as app
FROM arm64v8/debian:buster-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,8 +24,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
# curl: healcheck
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
ffmpeg \
libssl-dev \
@@ -35,7 +33,6 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
libomxil-bellagio0 \
libomxil-bellagio-bin \
locales \
curl \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -43,7 +40,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -59,8 +56,6 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM app
ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -70,6 +65,3 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/bin/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1

View File

@@ -10,7 +10,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -77,7 +76,7 @@ namespace DvdLib.Ifo
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
{
var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
var filename = string.Format("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));

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
@@ -11,29 +13,24 @@ namespace Emby.Dlna.ContentDirectory
/// Initializes a new instance of the <see cref="ServerItem"/> class.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="stubType">The stub type.</param>
public ServerItem(BaseItem item, StubType? stubType)
public ServerItem(BaseItem item)
{
Item = item;
if (stubType.HasValue)
{
StubType = stubType;
}
else if (item is IItemByName and not Folder)
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
/// <summary>
/// Gets the underlying base item.
/// Gets or sets the underlying base item.
/// </summary>
public BaseItem Item { get; }
public BaseItem Item { get; set; }
/// <summary>
/// Gets the DLNA item type.
/// Gets or sets the DLNA item type.
/// </summary>
public StubType? StubType { get; }
public StubType? StubType { get; set; }
}
}

View File

@@ -41,6 +41,8 @@ namespace Emby.Dlna.Didl
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
@@ -315,7 +317,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
}
if (filter.Contains("res@size"))
@@ -326,7 +328,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
}
}
}
@@ -340,7 +342,7 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
}
if (filter.Contains("res@resolution"))
@@ -359,12 +361,12 @@ namespace Emby.Dlna.Didl
if (targetSampleRate.HasValue)
{
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
}
if (totalBitrate.HasValue)
{
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
}
var mediaProfile = _profile.GetVideoMediaProfile(
@@ -550,7 +552,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
}
if (filter.Contains("res@size"))
@@ -561,7 +563,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
}
}
}
@@ -573,17 +575,17 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
}
if (targetSampleRate.HasValue)
{
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
}
if (targetAudioBitrate.HasValue)
{
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(
@@ -637,7 +639,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
var clientId = GetClientId(folder, stubType);
@@ -729,7 +731,7 @@ namespace Emby.Dlna.Didl
{
if (item.PremiereDate.HasValue)
{
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
}
}
@@ -746,7 +748,7 @@ namespace Emby.Dlna.Didl
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
}
if (item is not Folder)
if (!(item is Folder))
{
if (filter.Contains("dc:description"))
{
@@ -929,11 +931,11 @@ namespace Emby.Dlna.Didl
if (item.IndexNumber.HasValue)
{
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
if (item is Episode)
{
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
}
}
}

View File

@@ -17,7 +17,8 @@ namespace Emby.Dlna.Didl
public Filter(string filter)
{
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
}
public bool Contains(string field)

View File

@@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -83,7 +84,8 @@ namespace Emby.Dlna
{
lock (_profiles)
{
return _profiles.Values
var list = _profiles.Values.ToList();
return list
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Item1.Info.Name)
.Select(i => i.Item2)
@@ -110,7 +112,7 @@ namespace Emby.Dlna
if (profile == null)
{
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
LogUnmatchedProfile(deviceInfo);
}
else
{
@@ -120,6 +122,23 @@ namespace Emby.Dlna
return profile;
}
private void LogUnmatchedProfile(DeviceIdentification profile)
{
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
builder.Append("ModelName: ").AppendLine(profile.ModelName);
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
/// <summary>
/// Attempts to match a device with a profile.
/// Rules:
@@ -225,8 +244,11 @@ namespace Emby.Dlna
{
try
{
return _fileSystem.GetFilePaths(path)
var xmlFies = _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
.ToList();
return xmlFies
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
.ToList()!; // We just filtered out all the nulls
@@ -248,8 +270,11 @@ namespace Emby.Dlna
try
{
DeviceProfile profile;
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
var profile = ReserializeProfile(tempProfile);
profile = ReserializeProfile(tempProfile);
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -288,7 +313,8 @@ namespace Emby.Dlna
{
lock (_profiles)
{
return _profiles.Values
var list = _profiles.Values.ToList();
return list
.Select(i => i.Item1)
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Info.Name);
@@ -333,17 +359,14 @@ namespace Emby.Dlna
// The stream should exist as we just got its name from GetManifestResourceNames
using (var stream = _assembly.GetManifestResourceStream(name)!)
{
var length = stream.Length;
var fileInfo = _fileSystem.GetFileInfo(path);
if (!fileInfo.Exists || fileInfo.Length != length)
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
{
Directory.CreateDirectory(systemProfilesPath);
var fileOptions = AsyncFile.WriteOptions;
fileOptions.Mode = FileMode.Create;
fileOptions.PreallocationSize = length;
using (var fileStream = new FileStream(path, fileOptions))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -390,7 +413,7 @@ namespace Emby.Dlna
}
/// <inheritdoc />
public void UpdateProfile(string profileId, DeviceProfile profile)
public void UpdateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -404,7 +427,7 @@ namespace Emby.Dlna
throw new ArgumentException("Profile is missing Name");
}
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename);
@@ -463,22 +486,18 @@ namespace Emby.Dlna
}
/// <inheritdoc />
public ImageStream? GetIcon(string filename)
public ImageStream GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
var stream = _assembly.GetManifestResourceStream(resource);
if (stream == null)
{
return null;
}
return new ImageStream(stream)
return new ImageStream
{
Format = format
Format = format,
Stream = _assembly.GetManifestResourceStream(resource)
};
}

View File

@@ -17,19 +17,16 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
@@ -76,7 +73,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup>
</Project>

View File

@@ -11,7 +11,6 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
@@ -26,6 +25,8 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
@@ -81,7 +82,9 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
header = header.Split('-')[^1];
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
{
return val;
}
@@ -104,7 +107,7 @@ namespace Emby.Dlna.Eventing
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
return response;
}
@@ -161,7 +164,7 @@ namespace Emby.Dlna.Eventing
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
try
{

View File

@@ -52,6 +52,7 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled;
private PlayToManager _manager;
@@ -124,8 +125,8 @@ namespace Emby.Dlna.Main
config);
Current = this;
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
@@ -218,6 +219,11 @@ namespace Emby.Dlna.Main
}
}
private void LogMessage(string msg)
{
_logger.LogDebug(msg);
}
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{
try
@@ -262,11 +268,12 @@ namespace Emby.Dlna.Main
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
_networkManager,
MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
LogFunction = LogMessage,
SupportPnpRootDevice = false
};
@@ -311,9 +318,15 @@ namespace Emby.Dlna.Main
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";
uri.Port = _netConfig.HttpServerPortNumber;
}
var device = new SsdpRootDevice
{
@@ -399,6 +412,7 @@ namespace Emby.Dlna.Main
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
_config,
_userDataManager,
_localization,
_mediaSourceManager,

View File

@@ -20,6 +20,8 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
@@ -638,7 +640,7 @@ namespace Emby.Dlna.PlayTo
return;
}
Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
Volume = int.Parse(volumeValue, UsCulture);
if (Volume > 0)
{
@@ -840,7 +842,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
Duration = TimeSpan.Parse(duration, UsCulture);
}
else
{
@@ -852,7 +854,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
Position = TimeSpan.Parse(position, UsCulture);
}
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@@ -1179,7 +1181,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger);
}
#nullable enable
private static DeviceIcon CreateIcon(XElement element)
{
if (element == null)
@@ -1187,61 +1188,69 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(element));
}
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
_ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
_ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
return new DeviceIcon
{
Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
Depth = depth,
Height = heightValue,
MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
MimeType = mimeType,
Url = url,
Width = widthValue
};
}
private static DeviceService Create(XElement element)
=> new DeviceService()
{
ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty
};
{
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state)
return new DeviceService
{
ControlUrl = controlURL,
EventSubUrl = eventSubURL,
ScpdUrl = scpdUrl,
ServiceId = id,
ServiceType = type
};
}
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
{
TransportState = state;
var previousMediaInfo = CurrentMediaInfo;
CurrentMediaInfo = mediaInfo;
if (mediaInfo == null)
{
if (previousMediaInfo != null)
{
OnPlaybackStop(previousMediaInfo);
}
}
else if (previousMediaInfo == null)
if (previousMediaInfo == null && mediaInfo != null)
{
if (state != TransportState.Stopped)
{
OnPlaybackStart(mediaInfo);
}
}
else if (mediaInfo.Equals(previousMediaInfo))
{
OnPlaybackProgress(mediaInfo);
}
else
else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
{
OnMediaChanged(previousMediaInfo, mediaInfo);
}
else if (mediaInfo == null && previousMediaInfo != null)
{
OnPlaybackStop(previousMediaInfo);
}
else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
{
OnPlaybackProgress(mediaInfo);
}
}
private void OnPlaybackStart(UBaseObject mediaInfo)

View File

@@ -30,6 +30,8 @@ namespace Emby.Dlna.PlayTo
{
public class PlayToController : ISessionController, IDisposable
{
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@@ -210,9 +212,9 @@ namespace Emby.Dlna.PlayTo
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
var duration = mediaSource == null
? _device.Duration?.Ticks
: mediaSource.RunTimeTicks;
var duration = mediaSource == null ?
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
mediaSource.RunTimeTicks;
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
@@ -714,7 +716,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
return SetAudioStreamIndex(val);
}
@@ -726,7 +728,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
@@ -738,7 +740,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}

View File

@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@@ -34,6 +35,7 @@ namespace Emby.Dlna.PlayTo
private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
@@ -45,7 +47,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
_logger = logger;
_sessionManager = sessionManager;
@@ -56,6 +58,7 @@ namespace Emby.Dlna.PlayTo
_imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery;
_httpClientFactory = httpClientFactory;
_config = config;
_userDataManager = userDataManager;
_localization = localization;
_mediaSourceManager = mediaSourceManager;
@@ -170,9 +173,7 @@ namespace Emby.Dlna.PlayTo
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
var sessionInfo = await _sessionManager
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
.ConfigureAwait(false);
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();

View File

@@ -20,6 +20,8 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
@@ -43,12 +45,10 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await XDocument.LoadAsync(
stream,
LoadOptions.None,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
@@ -78,15 +78,14 @@ namespace Emby.Dlna.PlayTo
{
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
@@ -95,13 +94,12 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
LoadOptions.None,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
catch

View File

@@ -175,7 +175,7 @@ namespace Emby.Dlna.PlayTo
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
}
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);

View File

@@ -167,7 +167,8 @@ namespace Emby.Dlna.Profiles
public void AddXmlRootAttribute(string name, string value)
{
var list = XmlRootAttributes.ToList();
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
var list = atts.ToList();
list.Add(new XmlAttribute
{

View File

@@ -15,6 +15,7 @@ namespace Emby.Dlna.Server
{
private readonly DeviceProfile _profile;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly string _serverUdn;
private readonly string _serverAddress;
private readonly string _serverName;
@@ -189,16 +190,16 @@ namespace Emby.Dlna.Server
builder.Append("<icon>");
builder.Append("<mimetype>")
.Append(SecurityElement.Escape(icon.MimeType))
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
.Append("</mimetype>");
builder.Append("<width>")
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
.Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
.Append("</width>");
builder.Append("<height>")
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
.Append("</height>");
builder.Append("<depth>")
.Append(SecurityElement.Escape(icon.Depth))
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
.Append("</depth>");
builder.Append("<url>")
.Append(BuildUrl(icon.Url))
@@ -219,10 +220,10 @@ namespace Emby.Dlna.Server
builder.Append("<service>");
builder.Append("<serviceType>")
.Append(SecurityElement.Escape(service.ServiceType))
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
.Append("</serviceType>");
builder.Append("<serviceId>")
.Append(SecurityElement.Escape(service.ServiceId))
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("</serviceId>");
builder.Append("<SCPDURL>")
.Append(BuildUrl(service.ScpdUrl))
@@ -249,7 +250,8 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
return SecurityElement.Escape(url);
// TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
return SecurityElement.Escape(url) ?? string.Empty;
}
private IEnumerable<DeviceIcon> GetIcons()

View File

@@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
var settings = new XmlWriterSettings
{

View File

@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
return EventManager.CancelEventSubscription(subscriptionId);
}
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
{
return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
}
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
{
return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
}
}
}

View File

@@ -38,7 +38,7 @@ namespace Emby.Dlna.Service
builder.Append("<action>");
builder.Append("<name>")
.Append(SecurityElement.Escape(item.Name))
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<argumentList>");
@@ -48,13 +48,13 @@ namespace Emby.Dlna.Service
builder.Append("<argument>");
builder.Append("<name>")
.Append(SecurityElement.Escape(argument.Name))
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
.Append("</name>");
builder.Append("<direction>")
.Append(SecurityElement.Escape(argument.Direction))
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
.Append("</direction>");
builder.Append("<relatedStateVariable>")
.Append(SecurityElement.Escape(argument.RelatedStateVariable))
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
.Append("</relatedStateVariable>");
builder.Append("</argument>");
@@ -81,10 +81,10 @@ namespace Emby.Dlna.Service
.Append("\">");
builder.Append("<name>")
.Append(SecurityElement.Escape(item.Name))
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<dataType>")
.Append(SecurityElement.Escape(item.DataType))
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
.Append("</dataType>");
if (item.AllowedValues.Count > 0)

View File

@@ -6,13 +6,10 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<ItemGroup>
@@ -28,7 +25,7 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>

View File

@@ -3,7 +3,6 @@ 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;
@@ -27,7 +26,7 @@ namespace Emby.Drawing
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
private const char Version = '3';
private const string Version = "3";
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
@@ -102,7 +101,8 @@ namespace Emby.Drawing
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{
var file = await ProcessImage(options).ConfigureAwait(false);
using (var fileStream = AsyncFile.OpenRead(file.Path))
using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
@@ -117,7 +117,7 @@ namespace Emby.Drawing
=> _transparentImageTypes.Contains(Path.GetExtension(path));
/// <inheritdoc />
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
{
ItemImageInfo originalImage = options.Image;
BaseItem item = options.Item;
@@ -130,22 +130,20 @@ namespace Emby.Drawing
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
}
var mimeType = MimeTypes.GetMimeType(originalImagePath);
if (!_imageEncoder.SupportsImageEncoding)
{
return (originalImagePath, mimeType, dateModified);
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
originalImagePath = supportedImageInfo.Path;
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))
if (!File.Exists(originalImagePath))
{
return (originalImagePath, mimeType, dateModified);
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
dateModified = supportedImageInfo.DateModified;
dateModified = supportedImageInfo.dateModified;
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
bool autoOrient = false;
@@ -245,7 +243,7 @@ namespace Emby.Drawing
return ImageFormat.Jpg;
}
private string GetMimeType(ImageFormat format, string path)
private string? GetMimeType(ImageFormat format, string path)
=> format switch
{
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
@@ -439,7 +437,7 @@ namespace Emby.Drawing
.ToString("N", CultureInfo.InvariantCulture);
}
private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{
var inputFormat = Path.GetExtension(originalImagePath)
.TrimStart('.')

View File

@@ -14,7 +14,6 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
private readonly AudioBookResolver _audioBookResolver;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@@ -23,7 +22,6 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
_audioBookResolver = new AudioBookResolver(_options);
}
/// <summary>
@@ -33,18 +31,21 @@ namespace Emby.Naming.AudioBook
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
var audioBookResolver = new AudioBookResolver(_options);
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
.Select(i => _audioBookResolver.Resolve(i.FullName))
.Select(i => audioBookResolver.Resolve(i.FullName))
.OfType<AudioBookFileInfo>()
.ToList();
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
var stackResult = new StackResolver(_options)
.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
.Select(i => _audioBookResolver.Resolve(i))
.Select(i => audioBookResolver.Resolve(i))
.OfType<AudioBookFileInfo>()
.ToList();

View File

@@ -1,7 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.AudioBook
{
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
var extension = Path.GetExtension(path);
// Check supported extensions
if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return null;
}

View File

@@ -1,7 +1,4 @@
#pragma warning disable CA1819
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
@@ -125,11 +122,11 @@ namespace Emby.Naming.Common
token: "DSR")
};
VideoFileStackingRules = new[]
VideoFileStackingExpressions = 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)
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
};
CleanDateTimes = new[]
@@ -140,11 +137,8 @@ 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]|r3|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*$"
@"[ _\,\.\(\)\[\]\-](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]|r3|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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
SubtitleFileExtensions = new[]
@@ -256,8 +250,6 @@ namespace Emby.Naming.Common
},
// <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
// <!-- foo.E01., foo.e01. -->
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
@@ -376,20 +368,6 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
// Series and season only expression
// "the show/season 1", "the show/s01"
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
{
IsNamed = true
},
// Series and season only expression
// "the show S01", "the show season 1"
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
{
IsNamed = true
},
};
EpisodeWithoutSeasonExpressions = new[]
@@ -404,12 +382,6 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.DirectoryName,
"trailers",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
@@ -470,24 +442,12 @@ namespace Emby.Naming.Common
" sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeVideo,
ExtraRuleType.DirectoryName,
"backdrops",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.DirectoryName,
"theme-music",
MediaType.Audio),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
@@ -518,12 +478,6 @@ namespace Emby.Naming.Common
"-deleted",
MediaType.Video),
new ExtraRule(
ExtraType.DeletedScene,
ExtraRuleType.Suffix,
"-deletedscene",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
@@ -582,7 +536,7 @@ namespace Emby.Naming.Common
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
MediaType.Video)
MediaType.Video),
};
Format3DRules = new[]
@@ -694,29 +648,9 @@ namespace Emby.Naming.Common
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
{
["trailers"] = ExtraType.Trailer,
["theme-music"] = ExtraType.ThemeSong,
["backdrops"] = ExtraType.ThemeVideo,
["extras"] = ExtraType.Unknown,
["behind the scenes"] = ExtraType.BehindTheScenes,
["deleted scenes"] = ExtraType.DeletedScene,
["interviews"] = ExtraType.Interview,
["scenes"] = ExtraType.Scene,
["samples"] = ExtraType.Sample,
["shorts"] = ExtraType.Clip,
["featurettes"] = ExtraType.Clip
};
Compile();
}
/// <summary>
/// Gets or sets the folder name to extra types mapping.
/// </summary>
public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
/// <summary>
/// Gets or sets list of audio file extensions.
/// </summary>
@@ -798,9 +732,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; }
/// <summary>
/// Gets the file stacking rules.
/// Gets or sets list of raw video file-stacking expressions strings.
/// </summary>
public FileStackRule[] VideoFileStackingRules { get; }
public string[] VideoFileStackingExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -822,6 +756,11 @@ namespace Emby.Naming.Common
/// </summary>
public ExtraRule[] VideoExtraRules { get; set; }
/// <summary>
/// Gets list of video file-stack regular expressions.
/// </summary>
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
@@ -847,6 +786,7 @@ namespace Emby.Naming.Common
/// </summary>
public void Compile()
{
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();

View File

@@ -6,17 +6,14 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -42,13 +39,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>

View File

@@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Subtitles
{
@@ -35,7 +34,7 @@ namespace Emby.Naming.Subtitles
}
var extension = Path.GetExtension(path);
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return null;
}
@@ -43,11 +42,11 @@ namespace Emby.Naming.Subtitles
var flags = GetFlags(path);
var info = new SubtitleInfo(
path,
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
&& !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
.ToList();
// Should have a name, language and file extension

View File

@@ -1,8 +1,8 @@
using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Jellyfin.Extensions;
namespace Emby.Naming.TV
{
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
{
var extension = Path.GetExtension(path);
// Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))

View File

@@ -55,7 +55,7 @@ namespace Emby.Naming.TV
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
string path,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
if (result.SeasonNumber.HasValue)
if (result.seasonNumber.HasValue)
{
return result;
}
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
{
var numericStart = -1;
var length = 0;

View File

@@ -1,29 +0,0 @@
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for Series information.
/// </summary>
public class SeriesInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="SeriesInfo"/> class.
/// </summary>
/// <param name="path">Path to the file.</param>
public SeriesInfo(string path)
{
Path = path;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
public string Path { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string? Name { get; set; }
}
}

View File

@@ -1,60 +0,0 @@
using Emby.Naming.Common;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to parse information about series from paths containing more information that only the series name.
/// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
/// </summary>
public static class SeriesPathParser
{
/// <summary>
/// Parses information about series from path.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
/// <param name="path">Path.</param>
/// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
public static SeriesPathParserResult Parse(NamingOptions options, string path)
{
SeriesPathParserResult? result = null;
foreach (var expression in options.EpisodeExpressions)
{
var currentResult = Parse(path, expression);
if (currentResult.Success)
{
result = currentResult;
break;
}
}
if (result != null)
{
if (!string.IsNullOrEmpty(result.SeriesName))
{
result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
}
}
return result ?? new SeriesPathParserResult();
}
private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
{
var result = new SeriesPathParserResult();
var match = expression.Regex.Match(name);
if (match.Success && match.Groups.Count >= 3)
{
if (expression.IsNamed)
{
result.SeriesName = match.Groups["seriesname"].Value;
result.Success = !string.IsNullOrEmpty(result.SeriesName) && !match.Groups["seasonnumber"].ValueSpan.IsEmpty;
}
}
return result;
}
}
}

View File

@@ -1,19 +0,0 @@
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for <see cref="SeriesPathParser"/> result.
/// </summary>
public class SeriesPathParserResult
{
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string? SeriesName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether parsing was successful.
/// </summary>
public bool Success { get; set; }
}
}

View File

@@ -1,49 +0,0 @@
using System.IO;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to resolve information about series from path.
/// </summary>
public static class SeriesResolver
{
/// <summary>
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// 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,}))");
/// <summary>
/// Resolve information about series from path.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
/// <param name="path">Path to series.</param>
/// <returns>SeriesInfo.</returns>
public static SeriesInfo Resolve(NamingOptions options, string path)
{
string seriesName = Path.GetFileName(path);
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
if (result.Success)
{
if (!string.IsNullOrEmpty(result.SeriesName))
{
seriesName = result.SeriesName;
}
}
if (!string.IsNullOrEmpty(seriesName))
{
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
}
return new SeriesInfo(path)
{
Name = seriesName
};
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
@@ -16,39 +17,38 @@ namespace Emby.Naming.Video
/// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns>
public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out string newName)
public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{
if (string.IsNullOrEmpty(name))
{
newName = string.Empty;
newName = ReadOnlySpan<char>.Empty;
return false;
}
// Iteratively apply the regexps to clean the string.
bool cleaned = false;
for (int i = 0; i < expressions.Count; i++)
var len = expressions.Count;
for (int i = 0; i < len; i++)
{
if (TryClean(name, expressions[i], out newName))
{
cleaned = true;
name = newName;
return true;
}
}
newName = cleaned ? name : string.Empty;
return cleaned;
newName = ReadOnlySpan<char>.Empty;
return false;
}
private static bool TryClean(string name, Regex expression, out string newName)
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
{
var match = expression.Match(name);
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
int index = match.Index;
if (match.Success && index != 0)
{
newName = cleaned.Value;
newName = name.AsSpan().Slice(0, match.Index);
return true;
}
newName = string.Empty;
newName = ReadOnlySpan<char>.Empty;
return false;
}
}

View File

@@ -9,27 +9,44 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
public static class ExtraRuleResolver
public class ExtraResolver
{
private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
public ExtraResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
public ExtraResult GetExtraInfo(string path)
{
var result = new ExtraResult();
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
{
var rule = namingOptions.VideoExtraRules[i];
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
var rule = _options.VideoExtraRules[i];
if (rule.MediaType == MediaType.Audio)
{
continue;
if (!AudioFileParser.IsAudioFile(path, _options))
{
continue;
}
}
else if (rule.MediaType == MediaType.Video)
{
if (!VideoResolver.IsVideoFile(path, _options))
{
continue;
}
}
var pathSpan = path.AsSpan();
@@ -45,10 +62,9 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
// Trim the digits from the end of the filename so we can recognize things like -trailer2
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
var filename = Path.GetFileNameWithoutExtension(pathSpan);
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
@@ -58,9 +74,9 @@ namespace Emby.Naming.Video
{
var filename = Path.GetFileName(path);
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
if (isMatch)
if (regex.IsMatch(filename))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Jellyfin.Extensions;
using System.Linq;
namespace Emby.Naming.Video
{
@@ -12,30 +12,25 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="FileStack"/> class.
/// </summary>
/// <param name="name">The stack name.</param>
/// <param name="isDirectory">Whether the stack files are directories.</param>
/// <param name="files">The stack files.</param>
public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
public FileStack()
{
Name = name;
IsDirectoryStack = isDirectory;
Files = files;
Files = new List<string>();
}
/// <summary>
/// Gets the name of file stack.
/// Gets or sets name of file stack.
/// </summary>
public string Name { get; }
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets the list of paths in stack.
/// Gets or sets list of paths in stack.
/// </summary>
public IReadOnlyList<string> Files { get; }
public List<string> Files { get; set; }
/// <summary>
/// Gets a value indicating whether stack is directory stack.
/// Gets or sets a value indicating whether stack is directory stack.
/// </summary>
public bool IsDirectoryStack { get; }
public bool IsDirectoryStack { get; set; }
/// <summary>
/// Helper function to determine if path is in the stack.
@@ -45,12 +40,12 @@ namespace Emby.Naming.Video
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
if (string.IsNullOrEmpty(file))
if (IsDirectoryStack == isDirectory)
{
return false;
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
}
return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
return false;
}
}
}

View File

@@ -1,48 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video;
/// <summary>
/// Regex based rule for file stacking (eg. disc1, disc2).
/// </summary>
public class FileStackRule
{
private readonly Regex _tokenRegex;
/// <summary>
/// Initializes a new instance of the <see cref="FileStackRule"/> class.
/// </summary>
/// <param name="token">Token.</param>
/// <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);
IsNumerical = isNumerical;
}
/// <summary>
/// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
/// </summary>
public bool IsNumerical { get; }
/// <summary>
/// Match the input against the rule regex.
/// </summary>
/// <param name="input">The input.</param>
/// <param name="result">The part type and number or <c>null</c>.</param>
/// <returns>A value indicating whether the input matched the rule.</returns>
public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
{
result = null;
var match = _tokenRegex.Match(input);
if (!match.Success)
{
return false;
}
var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
return true;
}
}

View File

@@ -9,7 +9,7 @@ namespace Emby.Naming.Video
public static class Format3DParser
{
// Static default result to save on allocation costs.
private static readonly Format3DResult _defaultResult = new(false, null);
private static readonly Format3DResult _defaultResult = new (false, null);
/// <summary>
/// Parse 3D format related flags.

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@@ -11,28 +12,37 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
public static class StackResolver
public class StackResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="StackResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
public StackResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves only directories from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
}
/// <summary>
@@ -40,7 +50,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@@ -50,13 +60,19 @@ namespace Emby.Naming.Video
{
foreach (var file in directory)
{
var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
stack.Files.Add(file.Path);
yield return stack;
}
}
else
{
var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
foreach (var file in directory)
{
stack.Files.Add(file.Path);
}
yield return stack;
}
}
@@ -66,91 +82,158 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
var potentialFiles = files
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
.OrderBy(i => i.FullName);
var list = files
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
.OrderBy(i => i.FullName)
.ToList();
var potentialStacks = new Dictionary<string, StackMetadata>();
foreach (var file in potentialFiles)
var expressions = _options.VideoFileStackingRegexes;
for (var i = 0; i < list.Count; i++)
{
var name = file.Name;
if (string.IsNullOrEmpty(name))
var offset = 0;
var file1 = list[i];
var expressionIndex = 0;
while (expressionIndex < expressions.Length)
{
name = Path.GetFileName(file.FullName);
}
var exp = expressions[expressionIndex];
var stack = new FileStack();
for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
{
var rule = namingOptions.VideoFileStackingRules[i];
if (!rule.Match(name, out var stackParsingResult))
// (Title)(Volume)(Ignore)(Extension)
var match1 = FindMatch(file1, exp, offset);
if (match1.Success)
{
continue;
}
var title1 = match1.Groups["title"].Value;
var volume1 = match1.Groups["volume"].Value;
var ignore1 = match1.Groups["ignore"].Value;
var extension1 = match1.Groups["extension"].Value;
var stackName = stackParsingResult.Value.StackName;
var partNumber = stackParsingResult.Value.PartNumber;
var partType = stackParsingResult.Value.PartType;
if (!potentialStacks.TryGetValue(stackName, out var stackResult))
{
stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
potentialStacks[stackName] = stackResult;
}
if (stackResult.Parts.Count > 0)
{
if (stackResult.IsDirectory != file.IsDirectory
|| !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
|| stackResult.ContainsPart(partNumber))
var j = i + 1;
while (j < list.Count)
{
continue;
var file2 = list[j];
if (file1.IsDirectory != file2.IsDirectory)
{
j++;
continue;
}
// (Title)(Volume)(Ignore)(Extension)
var match2 = FindMatch(file2, exp, offset);
if (match2.Success)
{
var title2 = match2.Groups[1].Value;
var volume2 = match2.Groups[2].Value;
var ignore2 = match2.Groups[3].Value;
var extension2 = match2.Groups[4].Value;
if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
{
if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
{
if (stack.Files.Count == 0)
{
stack.Name = title1 + ignore1;
stack.IsDirectoryStack = file1.IsDirectory;
stack.Files.Add(file1.FullName);
}
stack.Files.Add(file2.FullName);
}
else
{
// Sequel
offset = 0;
expressionIndex++;
break;
}
}
else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
{
// False positive, try again with offset
offset = match1.Groups[3].Index;
break;
}
else
{
// Extension mismatch
offset = 0;
expressionIndex++;
break;
}
}
else
{
// Title mismatch
offset = 0;
expressionIndex++;
break;
}
}
else
{
// No match 2, next expression
offset = 0;
expressionIndex++;
break;
}
j++;
}
if (rule.IsNumerical != stackResult.IsNumerical)
if (j == list.Count)
{
break;
expressionIndex = expressions.Length;
}
}
else
{
// No match 1
offset = 0;
expressionIndex++;
}
stackResult.Parts.Add(partNumber, file);
break;
if (stack.Files.Count > 1)
{
yield return stack;
i += stack.Files.Count - 1;
break;
}
}
}
foreach (var (fileName, stack) in potentialStacks)
{
if (stack.Parts.Count < 2)
{
continue;
}
yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
}
}
private class StackMetadata
private static string GetRegexInput(FileSystemMetadata file)
{
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
// For directories, dummy up an extension otherwise the expressions will fail
var input = !file.IsDirectory
? file.FullName
: file.FullName + ".mkv";
return Path.GetFileName(input);
}
private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
{
var regexInput = GetRegexInput(input);
if (offset < 0 || offset >= regexInput.Length)
{
Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
IsDirectory = isDirectory;
IsNumerical = isNumerical;
PartType = partType;
return Match.Empty;
}
public Dictionary<string, FileSystemMetadata> Parts { get; }
public bool IsDirectory { get; }
public bool IsNumerical { get; }
public string PartType { get; }
public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
return regex.Match(regexInput, offset);
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path);
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return false;
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@@ -18,6 +17,7 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty<VideoFileInfo>();
Extras = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
}
@@ -39,15 +39,16 @@ namespace Emby.Naming.Video
/// <value>The files.</value>
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public IReadOnlyList<VideoFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
/// <summary>
/// Gets or sets the extra type.
/// </summary>
public ExtraType? ExtraType { get; set; }
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -16,41 +17,29 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="videoInfos">List of related video files.</param>
/// <param name="files">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
{
var videoInfos = files
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.OfType<VideoFileInfo>()
.ToList();
// 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)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var remainingFiles = new List<VideoFileInfo>();
var standaloneMedia = new List<VideoFileInfo>();
for (var i = 0; i < videoInfos.Count; i++)
{
var current = videoInfos[i];
if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
{
continue;
}
if (current.ExtraType == null)
{
standaloneMedia.Add(current);
}
else
{
remainingFiles.Add(current);
}
}
var remainingFiles = videoInfos
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
var list = new List<VideoInfo>();
@@ -58,20 +47,38 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
info.Extras = extras;
}
list.Add(info);
}
var standaloneMedia = remainingFiles
.Where(i => i.ExtraType == null)
.ToList();
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
remainingFiles.Remove(media);
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
list.Add(info);
}
@@ -80,12 +87,58 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions);
}
// If there's only one resolved video, use the folder name as well to find extras
if (list.Count == 1)
{
var info = list[0];
var videoPath = list[0].Files[0].Path;
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
if (!parentPath.IsEmpty)
{
var folderName = Path.GetFileName(parentPath);
if (!folderName.IsEmpty)
{
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
}
// Add the extras that are just based on file name as well
var extrasByFileName = remainingFiles
.Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
.ToList();
remainingFiles = remainingFiles
.Except(extrasByFileName)
.ToList();
extrasByFileName.AddRange(info.Extras);
info.Extras = extrasByFileName;
}
// If there's only one video, accept all trailers
// Be lenient because people use all kinds of mishmash conventions with trailers.
if (list.Count == 1)
{
var trailers = remainingFiles
.Where(i => i.ExtraType == ExtraType.Trailer)
.ToList();
trailers.AddRange(list[0].Extras);
list[0].Extras = trailers;
remainingFiles = remainingFiles
.Except(trailers)
.ToList();
}
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new[] { i },
Year = i.Year,
ExtraType = i.ExtraType
Year = i.Year
}));
return list;
@@ -109,11 +162,6 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
if (video.ExtraType != null)
{
continue;
}
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
return videos;
@@ -130,14 +178,17 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
extras.AddRange(video.Extras);
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
list[0].Extras = extras;
return list;
}
@@ -179,7 +230,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
tmpTestFilename = cleanName.Trim();
tmpTestFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
@@ -187,5 +238,67 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
{
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
}
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
{
if (baseName.IsEmpty)
{
return false;
}
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="baseName">The base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [baseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
{
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
}
/// <summary>
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
{
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
var result = new List<VideoFileInfo>();
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
{
var file = remainingFiles[pos];
if (file.ExtraType == null)
{
continue;
}
var filename = file.FileNameWithoutExtension;
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
{
result.Add(file);
remainingFiles.RemoveAt(pos);
}
}
return result;
}
}
}

View File

@@ -16,11 +16,10 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>VideoFileInfo.</returns>
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
{
return Resolve(path, true, namingOptions, parseName);
return Resolve(path, true, namingOptions);
}
/// <summary>
@@ -75,7 +74,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
var name = Path.GetFileNameWithoutExtension(path);
@@ -88,9 +87,9 @@ namespace Emby.Naming.Video
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
&& TryCleanString(name, namingOptions, out var newName))
&& TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
{
name = newName;
name = newName.ToString();
}
}
@@ -139,7 +138,7 @@ namespace Emby.Naming.Video
/// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}

View File

@@ -24,63 +24,63 @@ namespace Emby.Notifications
{
new NotificationTypeInfo
{
Type = nameof(NotificationType.ApplicationUpdateInstalled)
Type = NotificationType.ApplicationUpdateInstalled.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.InstallationFailed)
Type = NotificationType.InstallationFailed.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginInstalled)
Type = NotificationType.PluginInstalled.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginError)
Type = NotificationType.PluginError.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginUninstalled)
Type = NotificationType.PluginUninstalled.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.PluginUpdateInstalled)
Type = NotificationType.PluginUpdateInstalled.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.ServerRestartRequired)
Type = NotificationType.ServerRestartRequired.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.TaskFailed)
Type = NotificationType.TaskFailed.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.NewLibraryContent)
Type = NotificationType.NewLibraryContent.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.AudioPlayback)
Type = NotificationType.AudioPlayback.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.VideoPlayback)
Type = NotificationType.VideoPlayback.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.AudioPlaybackStopped)
Type = NotificationType.AudioPlaybackStopped.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.VideoPlaybackStopped)
Type = NotificationType.VideoPlaybackStopped.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.UserLockedOut)
Type = NotificationType.UserLockedOut.ToString()
},
new NotificationTypeInfo
{
Type = nameof(NotificationType.ApplicationUpdateAvailable)
Type = NotificationType.ApplicationUpdateAvailable.ToString()
}
};
@@ -98,7 +98,7 @@ namespace Emby.Notifications
private void Update(NotificationTypeInfo note)
{
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type) ?? note.Type;
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -24,7 +24,7 @@
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>

View File

@@ -5,7 +5,6 @@ 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;
@@ -105,7 +104,7 @@ namespace Emby.Notifications
var type = entry.Type;
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
{
return;
}

View File

@@ -19,14 +19,14 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>

View File

@@ -3,7 +3,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -61,7 +60,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
{
try
{

View File

@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
{
return _configurations.GetOrAdd(
key,
static (k, configurationManager) =>
(k, configurationManager) =>
{
var file = configurationManager.GetConfigurationFile(k);
@@ -371,7 +371,7 @@ namespace Emby.Server.Implementations.AppBase
NewConfiguration = configuration
});
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
_configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
var path = GetConfigurationFile(key);
Directory.CreateDirectory(Path.GetDirectoryName(path));

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -40,19 +41,20 @@ namespace Emby.Server.Implementations.AppBase
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
byte[] newBytes = stream.GetBuffer();
int newBytesLen = (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 == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes);
fs.Write(newBytes, 0, newBytesLen);
}
}

View File

@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -19,7 +18,6 @@ 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.Archiving;
@@ -40,6 +38,7 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
@@ -58,9 +57,9 @@ using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -76,6 +75,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@@ -117,11 +117,6 @@ namespace Emby.Server.Implementations
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
/// <summary>
/// The disposable parts.
/// </summary>
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
@@ -131,57 +126,7 @@ namespace Emby.Server.Implementations
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
/// <summary>
/// Gets or sets all concrete types.
/// </summary>
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
private DeviceId _deviceId;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig)
{
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
private string[] _urlPrefixes;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
@@ -211,7 +156,12 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; private set; }
public INetworkManager NetManager { get; internal set; }
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
@@ -227,22 +177,35 @@ namespace Emby.Server.Implementations
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
/// <summary>
/// Gets the logger factory.
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
/// Gets the application paths.
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; }
protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets the configuration manager.
/// Gets or sets all concrete types.
/// </summary>
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
/// <summary>
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
/// <summary>
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
public ServerConfigurationManager ConfigurationManager { get; }
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
@@ -264,6 +227,79 @@ namespace Emby.Server.Implementations
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = fileSystem;
ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
/// <summary>
/// Temporary function to migration network settings out of system.xml and into network.xml.
/// TODO: remove at the point when a fixed migration path has been decided upon.
/// </summary>
private void MigrateNetworkConfiguration()
{
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
Logger.LogDebug("Successfully migrated network settings.");
}
}
public string ExpandVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
public string ReverseVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
public Version ApplicationVersion { get; }
@@ -288,6 +324,8 @@ namespace Emby.Server.Implementations
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
private DeviceId _deviceId;
public string SystemId
{
get
@@ -301,33 +339,21 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string Name => ApplicationProductName;
private string CertificatePath { get; set; }
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>System.Object.</returns>
public object CreateInstance(Type type)
=> ActivatorUtilities.CreateInstance(ServiceProvider, type);
public X509Certificate2 Certificate { get; private set; }
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
public string FriendlyName =>
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
public string ExpandVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
public string ReverseVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>T.</returns>
public T CreateInstance<T>()
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
/// <summary>
/// Creates the instance safe.
@@ -338,7 +364,7 @@ namespace Emby.Server.Implementations
{
_creatingInstances ??= new List<Type>();
if (_creatingInstances.Contains(type))
if (_creatingInstances.IndexOf(type) != -1)
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
@@ -348,7 +374,7 @@ namespace Emby.Server.Implementations
_pluginManager.FailPlugin(type.Assembly);
throw new TypeLoadException("DI Loop detected");
throw new ExternalException("DI Loop detected.");
}
try
@@ -381,15 +407,8 @@ namespace Emby.Server.Implementations
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
var numberOfConcreteTypes = _allConcreteTypes.Length;
for (var i = 0; i < numberOfConcreteTypes; i++)
{
var type = _allConcreteTypes[i];
if (currentType.IsAssignableFrom(type))
{
yield return type;
}
}
return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
}
/// <inheritdoc />
@@ -404,9 +423,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
foreach (var part in parts.OfType<IDisposable>())
lock (_disposableParts)
{
_disposableParts.TryAdd(part, byte.MinValue);
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
@@ -425,9 +444,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
foreach (var part in parts.OfType<IDisposable>())
lock (_disposableParts)
{
_disposableParts.TryAdd(part, byte.MinValue);
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
@@ -437,7 +456,6 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
@@ -451,7 +469,7 @@ namespace Emby.Server.Implementations
_mediaEncoder.SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
Logger.LogInformation("ServerId: {0}", SystemId);
var entryPoints = GetExports<IServerEntryPoint>();
@@ -491,12 +509,14 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
public void Init(IServiceCollection serviceCollection)
public void Init()
{
DiscoverTypes();
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
@@ -516,133 +536,140 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
CertificatePath = networkConfiguration.CertificatePath;
Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
CertificateInfo = new CertificateInfo
{
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
RegisterServices(serviceCollection);
RegisterServices();
_pluginManager.RegisterServices(serviceCollection);
_pluginManager.RegisterServices(ServiceCollection);
}
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
/// </summary>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
protected virtual void RegisterServices(IServiceCollection serviceCollection)
protected virtual void RegisterServices()
{
serviceCollection.AddSingleton(_startupOptions);
ServiceCollection.AddSingleton(_startupOptions);
serviceCollection.AddMemoryCache();
ServiceCollection.AddMemoryCache();
serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager);
ServiceCollection.AddSingleton(NetManager);
serviceCollection.AddSingleton<ITaskManager, TaskManager>();
ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
serviceCollection.AddSingleton(_xmlSerializer);
ServiceCollection.AddSingleton(_xmlSerializer);
serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
serviceCollection.AddSingleton<IZipClient, ZipClient>();
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
serviceCollection.AddSingleton<IServerApplicationHost>(this);
serviceCollection.AddSingleton(ApplicationPaths);
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<NamingOptions>();
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
serviceCollection.AddSingleton<IDtoService, DtoService>();
ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
ServiceCollection.AddSingleton<IDtoService, DtoService>();
serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
serviceCollection.AddSingleton<LiveTvDtoService>();
serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
ServiceCollection.AddSingleton<LiveTvDtoService>();
ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
serviceCollection.AddSingleton<IAuthService, AuthService>();
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
serviceCollection.AddSingleton<TranscodingJobHelper>();
serviceCollection.AddScoped<MediaInfoHelper>();
serviceCollection.AddScoped<AudioHelper>();
serviceCollection.AddScoped<DynamicHlsHelper>();
serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>();
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
}
/// <summary>
@@ -657,6 +684,8 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
@@ -695,27 +724,30 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
private X509Certificate2 GetCertificate(string path, string password)
private X509Certificate2 GetCertificate(CertificateInfo info)
{
if (string.IsNullOrWhiteSpace(path))
var certificateLocation = info?.Path;
if (string.IsNullOrWhiteSpace(certificateLocation))
{
return null;
}
try
{
if (!File.Exists(path))
if (!File.Exists(certificateLocation))
{
return null;
}
// Don't use an empty string password
password = string.IsNullOrWhiteSpace(password) ? null : password;
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey)
{
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation);
return null;
}
@@ -723,7 +755,7 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation);
return null;
}
}
@@ -765,6 +797,8 @@ namespace Emby.Server.Implementations
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(),
@@ -832,12 +866,36 @@ namespace Emby.Server.Implementations
}
}
private CertificateInfo CertificateInfo { get; set; }
public X509Certificate2 Certificate { get; private set; }
private IEnumerable<string> GetUrlPrefixes()
{
var hosts = new[] { "+" };
return hosts.SelectMany(i =>
{
var prefixes = new List<string>
{
"http://" + i + ":" + HttpPort + "/"
};
if (CertificateInfo != null)
{
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
}
return prefixes;
});
}
/// <summary>
/// Called when [configuration updated].
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
private void OnConfigurationUpdated(object sender, EventArgs e)
protected void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@@ -846,8 +904,8 @@ namespace Emby.Server.Implementations
if (HttpPort != 0 && HttpsPort != 0)
{
// Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort
|| networkConfiguration.HttpsPortNumber != HttpsPort)
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort)
{
if (ConfigurationManager.Configuration.IsPortAuthorized)
{
@@ -859,6 +917,11 @@ namespace Emby.Server.Implementations
}
}
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
@@ -882,7 +945,7 @@ namespace Emby.Server.Implementations
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
@@ -900,7 +963,7 @@ namespace Emby.Server.Implementations
}
/// <summary>
/// Notifies the kernel that a change has been made that requires a restart.
/// Notifies that the kernel that a change has been made that requires a restart.
/// </summary>
public void NotifyPendingRestart()
{
@@ -1010,9 +1073,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the system status.
/// </summary>
/// <param name="request">Where this request originated.</param>
/// <param name="source">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
public SystemInfo GetSystemInfo(HttpRequest request)
public SystemInfo GetSystemInfo(IPAddress source)
{
return new SystemInfo
{
@@ -1034,14 +1097,20 @@ namespace Emby.Server.Implementations
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
LocalAddress = GetSmartApiUrl(source),
SupportsLibraryMonitor = true,
EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
};
}
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
=> NetManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i))
.ToList();
public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
{
return new PublicSystemInfo
{
@@ -1050,13 +1119,16 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
LocalAddress = GetSmartApiUrl(address),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr)
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1065,25 +1137,19 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(remoteAddr, out var port);
string smart = NetManager.GetBindInterface(remoteAddr, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
/// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request)
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
// Return the host in the HTTP request as the API url
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{
int? requestPort = request.Host.Port;
if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
{
requestPort = -1;
}
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
}
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
@@ -1091,12 +1157,18 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(request, out var port);
string smart = NetManager.GetBindInterface(request, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
}
/// <inheritdoc/>
public string GetSmartApiUrl(string hostname)
public string GetSmartApiUrl(string hostname, int? port = null)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1105,29 +1177,31 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(hostname, out var port);
string smart = NetManager.GetBindInterface(hostname, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
/// <inheritdoc/>
public string GetApiUrlForLocalAccess(bool allowHttps = true)
public string GetLoopbackHttpApiUrl()
{
// With an empty source, the port will be null
string smart = NetManager.GetBindInterface(string.Empty, out _);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
if (NetManager.IsIP6Enabled)
{
return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
}
return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
}
/// <inheritdoc/>
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return hostname.TrimEnd('/');
}
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash.
return new UriBuilder
@@ -1139,7 +1213,14 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/');
}
/// <inheritdoc />
public string FriendlyName =>
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Shuts down.
/// </summary>
public async Task Shutdown()
{
if (IsShuttingDown)
@@ -1177,7 +1258,41 @@ namespace Emby.Server.Implementations
}
}
/// <inheritdoc />
public virtual void LaunchUrl(string url)
{
if (!CanLaunchWebBrowser)
{
throw new NotSupportedException();
}
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true,
ErrorDialog = false
},
EnableRaisingEvents = true
};
process.Exited += (sender, args) => ((Process)sender).Dispose();
try
{
process.Start();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error launching url: {url}", url);
throw;
}
}
private bool _disposed = false;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
@@ -1201,15 +1316,12 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name);
foreach (var (part, _) in _disposableParts)
{
var partType = part.GetType();
if (partType == type)
{
continue;
}
var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList();
_disposableParts.Clear();
Logger.LogInformation("Disposing {Type}", partType.Name);
foreach (var part in parts)
{
Logger.LogInformation("Disposing {Type}", part.GetType().Name);
try
{
@@ -1217,14 +1329,19 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
}
}
_disposableParts.Clear();
}
_disposed = true;
}
}
internal class CertificateInfo
{
public string Path { get; set; }
public string Password { get; set; }
}
}

View File

@@ -1,8 +1,11 @@
using System.IO;
using MediaBrowser.Model.IO;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
using SharpCompress.Readers.Zip;
namespace Emby.Server.Implementations.Archiving
{
@@ -11,6 +14,53 @@ namespace Emby.Server.Implementations.Archiving
/// </summary>
public class ZipClient : IZipClient
{
/// <summary>
/// Extracts all.
/// </summary>
/// <param name="sourceFile">The source file.</param>
/// <param name="targetPath">The target path.</param>
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
{
using var fileStream = File.OpenRead(sourceFile);
ExtractAll(fileStream, targetPath, overwriteExistingFiles);
}
/// <summary>
/// Extracts all.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="targetPath">The target path.</param>
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
{
using var reader = ReaderFactory.Open(source);
var options = new ExtractionOptions
{
ExtractFullPath = true
};
if (overwriteExistingFiles)
{
options.Overwrite = true;
}
reader.WriteAllToDirectory(targetPath, options);
}
/// <inheritdoc />
public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
{
using var reader = ZipReader.Open(source);
var options = new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = overwriteExistingFiles
};
reader.WriteAllToDirectory(targetPath, options);
}
/// <inheritdoc />
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
{
@@ -21,7 +71,6 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -42,5 +91,67 @@ namespace Emby.Server.Implementations.Archiving
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
}
}
/// <summary>
/// Extracts all from7z.
/// </summary>
/// <param name="sourceFile">The source file.</param>
/// <param name="targetPath">The target path.</param>
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
{
using var fileStream = File.OpenRead(sourceFile);
ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
}
/// <summary>
/// Extracts all from7z.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="targetPath">The target path.</param>
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
{
using var archive = SevenZipArchive.Open(source);
using var reader = archive.ExtractAllEntries();
var options = new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = overwriteExistingFiles
};
reader.WriteAllToDirectory(targetPath, options);
}
/// <summary>
/// Extracts all from tar.
/// </summary>
/// <param name="sourceFile">The source file.</param>
/// <param name="targetPath">The target path.</param>
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
{
using var fileStream = File.OpenRead(sourceFile);
ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
}
/// <summary>
/// Extracts all from tar.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="targetPath">The target path.</param>
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
{
using var archive = TarArchive.Open(source);
using var reader = archive.ExtractAllEntries();
var options = new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = overwriteExistingFiles
};
reader.WriteAllToDirectory(targetPath, options);
}
}
}

View File

@@ -10,9 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.Channels
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
return channel is not IDisableMediaSourceDisplay;
return !(channel is IDisableMediaSourceDisplay);
}
/// <inheritdoc />
@@ -130,14 +129,16 @@ namespace Emby.Server.Implementations.Channels
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
if (internalChannel == null)
{
throw new ArgumentException(nameof(item.ChannelId));
throw new ArgumentException();
}
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
if (channel is not ISupportsDelete supportsDelete)
var supportsDelete = channel as ISupportsDelete;
if (supportsDelete == null)
{
throw new ArgumentException(nameof(channel));
throw new ArgumentException();
}
return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
@@ -178,7 +179,7 @@ namespace Emby.Server.Implementations.Channels
try
{
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
&& hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
&& hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
}
catch
{
@@ -540,7 +541,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Channel },
IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i)).ToArray();
}
@@ -585,7 +586,7 @@ namespace Emby.Server.Implementations.Channels
{
var supportsLatest = provider is ISupportsLatestMedia;
return new ChannelFeatures(channel.Name, channel.Id)
return new ChannelFeatures
{
CanFilter = !features.MaxPageSize.HasValue,
CanSearch = provider is ISearchableChannel,
@@ -595,6 +596,8 @@ namespace Emby.Server.Implementations.Channels
MediaTypes = features.MediaTypes.ToArray(),
SupportsSortOrderToggle = features.SupportsSortOrderToggle,
SupportsLatestMedia = supportsLatest,
Name = channel.Name,
Id = channel.Id.ToString("N", CultureInfo.InvariantCulture),
SupportsContentDownloading = features.SupportsContentDownloading,
AutoRefreshLevels = features.AutoRefreshLevels
};
@@ -812,7 +815,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
await using FileStream jsonStream = File.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -835,7 +838,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
await using FileStream jsonStream = File.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -1074,6 +1077,14 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true;
}
// was used for status
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
//{
// item.ExternalEtag = info.Etag;
// forceUpdate = true;
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
//}
if (!internalChannelId.Equals(item.ChannelId))
{
forceUpdate = true;
@@ -1134,7 +1145,7 @@ namespace Emby.Server.Implementations.Channels
if (!info.IsLiveStream)
{
if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
@@ -1143,7 +1154,7 @@ namespace Emby.Server.Implementations.Channels
}
else
{
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);

View File

@@ -2,7 +2,6 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -52,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Channel },
IncludeItemTypes = new[] { nameof(Channel) },
ExcludeItemIds = installedChannelIds.ToArray()
});

View File

@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions
{
PathInfos = new[] { new MediaPathInfo(path) },
PathInfos = new[] { new MediaPathInfo { Path = path } },
EnableRealtimeMonitor = false,
SaveLocalMetadata = true
};
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Collections
if (parentFolder == null)
{
throw new ArgumentException(nameof(parentFolder));
throw new ArgumentException();
}
var path = Path.Combine(parentFolder.Path, folderName);
@@ -196,8 +196,8 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
=> AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{

View File

@@ -1,20 +1,17 @@
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;
using static MediaBrowser.Common.Cryptography.Constants;
namespace Emby.Server.Implementations.Cryptography
{
/// <summary>
/// Class providing abstractions over cryptographic functions.
/// </summary>
public class CryptographyProvider : ICryptoProvider
public class CryptographyProvider : ICryptoProvider, IDisposable
{
// TODO: remove when not needed for backwards compat
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
@@ -33,71 +30,71 @@ namespace Emby.Server.Implementations.Cryptography
"System.Security.Cryptography.SHA512"
};
/// <inheritdoc />
public string DefaultHashMethod => "PBKDF2-SHA512";
private RandomNumberGenerator _randomNumberGenerator;
/// <inheritdoc />
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="CryptographyProvider"/> class.
/// </summary>
public CryptographyProvider()
{
byte[] salt = GenerateSalt();
return new PasswordHash(
DefaultHashMethod,
Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
DefaultIterations,
HashAlgorithmName.SHA512,
DefaultOutputLength),
salt,
new Dictionary<string, string>
{
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
});
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
// Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
// there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
// Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
_randomNumberGenerator = RandomNumberGenerator.Create();
}
/// <inheritdoc />
public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
public string DefaultHashMethod => "PBKDF2";
/// <inheritdoc />
public IEnumerable<string> GetSupportedHashMethods()
=> _supportedHashMethods;
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
{
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
// downgrading for now as we need this library to be dotnetstandard compliant
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
if (method != DefaultHashMethod)
{
return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2(
password,
hash.Salt,
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA1,
32));
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
}
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
return r.GetBytes(32);
}
/// <inheritdoc />
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
{
if (hashMethod == DefaultHashMethod)
{
return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2(
password,
hash.Salt,
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA512,
DefaultOutputLength));
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
}
if (!_supportedHashMethods.Contains(hash.Id))
if (!_supportedHashMethods.Contains(hashMethod))
{
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
}
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)
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
if (salt.Length == 0)
{
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
return h.ComputeHash(bytes);
}
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
byte[] salted = new byte[bytes.Length + salt.Length];
Array.Copy(bytes, salted, bytes.Length);
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
return h.ComputeHash(salted);
}
/// <inheritdoc />
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
/// <inheritdoc />
public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength);
@@ -105,10 +102,35 @@ namespace Emby.Server.Implementations.Cryptography
/// <inheritdoc />
public byte[] GenerateSalt(int length)
{
var salt = new byte[length];
using var rng = RandomNumberGenerator.Create();
rng.GetNonZeroBytes(salt);
byte[] salt = new byte[length];
_randomNumberGenerator.GetBytes(salt);
return salt;
}
/// <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)
{
_randomNumberGenerator.Dispose();
}
_disposed = true;
}
}
}

View File

@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Data
protected virtual int? CacheSize => null;
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// 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";
@@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
protected ManagedConnection GetConnection(bool readOnly = false)
protected ManagedConnection GetConnection(bool _ = false)
{
WriteLock.Wait();
if (WriteConnection != null)
@@ -160,22 +160,21 @@ namespace Emby.Server.Implementations.Data
protected bool TableExists(ManagedConnection connection, string name)
{
return connection.RunInTransaction(
db =>
db =>
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
foreach (var row in statement.ExecuteQuery())
{
foreach (var row in statement.ExecuteQuery())
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
return true;
}
return true;
}
}
}
return false;
},
ReadTransactionMode);
return false;
}, ReadTransactionMode);
}
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
@@ -195,7 +194,7 @@ namespace Emby.Server.Implementations.Data
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
{
return;
}
@@ -250,4 +249,55 @@ namespace Emby.Server.Implementations.Data
_disposed = true;
}
}
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
/// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}
}

View File

@@ -7,12 +7,10 @@ using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public sealed class ManagedConnection : IDisposable
public class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim _writeLock;
private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)

View File

@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AdjustToUniversal);
DateTimeStyles.None).ToUniversalTime();
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
var dateText = item.ToString();
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
{
result = dateTimeResult;
result = dateTimeResult.ToUniversalTime();
return true;
}

File diff suppressed because it is too large Load Diff

View File

@@ -32,9 +32,6 @@ namespace Emby.Server.Implementations.Data
/// <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)
{
WriteLock.Dispose();
@@ -50,42 +47,41 @@ namespace Emby.Server.Implementations.Data
var users = userDatasTableExists ? null : userManager.Users;
connection.RunInTransaction(
db =>
db =>
{
db.ExecuteAll(string.Join(';', new[] {
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
}));
if (userDataTableExists)
{
db.ExecuteAll(string.Join(';', new[]
var existingColumnNames = GetColumnNames(db, "userdata");
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (!userDatasTableExists)
{
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
ImportUserIds(db, users);
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
}));
if (userDataTableExists)
{
var existingColumnNames = GetColumnNames(db, "userdata");
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (!userDatasTableExists)
{
ImportUserIds(db, users);
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
}
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
}
},
TransactionMode);
}
}, TransactionMode);
}
}
@@ -133,17 +129,19 @@ namespace Emby.Server.Implementations.Data
return list;
}
/// <inheritdoc />
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
/// <summary>
/// Saves the user data.
/// </summary>
public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
if (userId <= 0)
if (internalUserId <= 0)
{
throw new ArgumentNullException(nameof(userId));
throw new ArgumentNullException(nameof(internalUserId));
}
if (string.IsNullOrEmpty(key))
@@ -151,23 +149,22 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(key));
}
PersistUserData(userId, key, userData, cancellationToken);
PersistUserData(internalUserId, key, userData, cancellationToken);
}
/// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
if (userId <= 0)
if (internalUserId <= 0)
{
throw new ArgumentNullException(nameof(userId));
throw new ArgumentNullException(nameof(internalUserId));
}
PersistAllUserData(userId, userData, cancellationToken);
PersistAllUserData(internalUserId, userData, cancellationToken);
}
/// <summary>
@@ -177,6 +174,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -184,11 +182,10 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
SaveUserData(db, internalUserId, key, userData);
},
TransactionMode);
db =>
{
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
}
}
@@ -254,33 +251,32 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
db =>
{
foreach (var userItemData in userDataList)
{
foreach (var userItemData in userDataList)
{
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
}
},
TransactionMode);
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
}
}, TransactionMode);
}
}
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="internalUserId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
/// key.
/// key
/// </exception>
public UserItemData GetUserData(long userId, string key)
public UserItemData GetUserData(long internalUserId, string key)
{
if (userId <= 0)
if (internalUserId <= 0)
{
throw new ArgumentNullException(nameof(userId));
throw new ArgumentNullException(nameof(internalUserId));
}
if (string.IsNullOrEmpty(key))
@@ -292,7 +288,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
statement.TryBind("@UserId", userId);
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
@@ -305,7 +301,7 @@ namespace Emby.Server.Implementations.Data
}
}
public UserItemData GetUserData(long userId, List<string> keys)
public UserItemData GetUserData(long internalUserId, List<string> keys)
{
if (keys == null)
{
@@ -317,19 +313,19 @@ namespace Emby.Server.Implementations.Data
return null;
}
return GetUserData(userId, keys[0]);
return GetUserData(internalUserId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
/// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long userId)
/// <param name="internalUserId"></param>
/// <returns></returns>
public List<UserItemData> GetAllUserData(long internalUserId)
{
if (userId <= 0)
if (internalUserId <= 0)
{
throw new ArgumentNullException(nameof(userId));
throw new ArgumentNullException(nameof(internalUserId));
}
var list = new List<UserItemData>();
@@ -338,7 +334,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", userId);
statement.TryBind("@UserId", internalUserId);
foreach (var row in statement.ExecuteQuery())
{
@@ -353,8 +349,7 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
/// <param name="reader"></param>
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
{
var userData = new UserItemData();

View File

@@ -1,30 +0,0 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
/// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}

View File

@@ -1,23 +0,0 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}

View File

@@ -15,18 +15,9 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
private readonly object _syncLock = new object();
private string _id;
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
{
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DeviceId>();
}
public string Value => _id ?? (_id = GetDeviceId());
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
private string GetCachedId()
@@ -37,7 +28,7 @@ namespace Emby.Server.Implementations.Devices
{
var value = File.ReadAllText(CachePath, Encoding.UTF8);
if (Guid.TryParse(value, out _))
if (Guid.TryParse(value, out var guid))
{
return value;
}
@@ -95,5 +86,15 @@ namespace Emby.Server.Implementations.Devices
return id;
}
private string _id;
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
{
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DeviceId>();
}
public string Value => _id ?? (_id = GetDeviceId());
}
}

View File

@@ -0,0 +1,146 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
private readonly IUserManager _userManager;
private readonly IAuthenticationRepository _authRepo;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
{
_userManager = userManager;
_authRepo = authRepo;
}
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
_capabilitiesMap[deviceId] = capabilities;
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
_authRepo.UpdateDeviceOptions(deviceId, options);
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
return _authRepo.GetDeviceOptions(deviceId);
}
public ClientCapabilities GetCapabilities(string id)
{
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
? result
: new ClientCapabilities();
}
public DeviceInfo GetDevice(string id)
{
var session = _authRepo.Get(new AuthenticationInfoQuery
{
DeviceId = id
}).Items.FirstOrDefault();
var device = session == null ? null : ToDeviceInfo(session);
return device;
}
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
{
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
{
// UserId = query.UserId
HasUser = true
}).Items;
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
if (query.SupportsSync.HasValue)
{
var val = query.SupportsSync.Value;
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
}
if (!query.UserId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(query.UserId);
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
var array = sessions.Select(ToDeviceInfo).ToArray();
return new QueryResult<DeviceInfo>(array);
}
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
{
var caps = GetCapabilities(authInfo.DeviceId);
return new DeviceInfo
{
AppName = authInfo.AppName,
AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId,
LastUserId = authInfo.UserId,
LastUserName = authInfo.UserName,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps?.IconUrl
};
}
public bool CanAccessDevice(User user, string deviceId)
{
if (user == null)
{
throw new ArgumentException("user not found");
}
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException(nameof(deviceId));
}
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
{
return true;
}
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
if (capabilities != null && capabilities.SupportsPersistentIdentifier)
{
return false;
}
}
return true;
}
}
}

View File

@@ -7,9 +7,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
@@ -51,6 +51,8 @@ namespace Emby.Server.Implementations.Dto
private readonly IMediaSourceManager _mediaSourceManager;
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
public DtoService(
ILogger<DtoService> logger,
ILibraryManager libraryManager,
@@ -73,8 +75,6 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.Dto
}
});
SetItemByNameInfo(item, dto, libraryItems);
SetItemByNameInfo(item, dto, libraryItems, user);
}
}
@@ -134,11 +134,14 @@ namespace Emby.Server.Implementations.Dto
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
{
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
LivetvManager.AddChannelInfo(list, options, user);
}
else if (item is LiveTvProgram)
{
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
Task.WaitAll(task);
}
if (item is IItemByName itemByName
@@ -153,7 +156,8 @@ namespace Emby.Server.Implementations.Dto
new DtoOptions(false)
{
EnableImages = false
}));
}),
user);
}
return dto;
@@ -293,7 +297,7 @@ namespace Emby.Server.Implementations.Dto
path = path.TrimStart('.');
}
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
{
fileExtensionContainer = path;
}
@@ -310,13 +314,13 @@ namespace Emby.Server.Implementations.Dto
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
{
SetItemByNameInfo(item, dto, taggedItems);
SetItemByNameInfo(item, dto, taggedItems, user);
}
return dto;
}
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems, User user = null)
{
if (item is MusicArtist)
{
@@ -369,12 +373,6 @@ namespace Emby.Server.Implementations.Dto
if (item is MusicAlbum || item is Season || item is Playlist)
{
dto.ChildCount = dto.RecursiveItemCount;
var folderChildCount = folder.LinkedChildren.Length;
// The default is an empty array, so we can't reliably use the count when it's empty
if (folderChildCount > 0)
{
dto.ChildCount ??= folderChildCount;
}
}
if (options.ContainsField(ItemFields.ChildCount))
@@ -422,7 +420,7 @@ namespace Emby.Server.Implementations.Dto
// Just return something so that apps that are expecting a value won't think the folders are empty
if (folder is ICollectionFolder || folder is UserView)
{
return Random.Shared.Next(1, 10);
return new Random().Next(1, 10);
}
return folder.GetChildCount(user);
@@ -469,7 +467,7 @@ namespace Emby.Server.Implementations.Dto
{
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
IncludeItemTypes = new[] { nameof(MusicAlbum) },
Name = item.Album,
Limit = 1
});
@@ -499,7 +497,7 @@ namespace Emby.Server.Implementations.Dto
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
_logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path);
return null;
}
}
@@ -509,6 +507,7 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachPeople(BaseItemDto dto, BaseItem item)
{
// Ordering by person type to ensure actors and artists are at the front.
@@ -617,6 +616,7 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachStudios(BaseItemDto dto, BaseItem item)
{
dto.Studios = item.Studios
@@ -757,6 +757,15 @@ namespace Emby.Server.Implementations.Dto
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
}
if (options.ContainsField(ItemFields.ScreenshotImageTags))
{
var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
if (screenshotLimit > 0)
{
dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
}
}
if (options.ContainsField(ItemFields.Genres))
{
dto.Genres = item.Genres;
@@ -798,7 +807,7 @@ namespace Emby.Server.Implementations.Dto
dto.MediaType = item.MediaType;
if (item is not LiveTvProgram)
if (!(item is LiveTvProgram))
{
dto.LocationType = item.LocationType;
}
@@ -919,9 +928,9 @@ namespace Emby.Server.Implementations.Dto
}
// if (options.ContainsField(ItemFields.MediaSourceCount))
// {
//{
// Songs always have one
// }
//}
}
if (item is IHasArtist hasArtist)
@@ -929,10 +938,10 @@ namespace Emby.Server.Implementations.Dto
dto.Artists = hasArtist.Artists;
// var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
// {
//{
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
// });
//});
// dto.ArtistItems = artistItems.Items
// .Select(i =>
@@ -949,7 +958,7 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = hasArtist.Artists
// .Except(foundArtists, new DistinctNameComparer())
//.Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
// This should not be necessary but we're seeing some cases of it
@@ -981,10 +990,10 @@ namespace Emby.Server.Implementations.Dto
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
// var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
// {
//{
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
// });
//});
// dto.AlbumArtists = artistItems.Items
// .Select(i =>
@@ -999,7 +1008,7 @@ namespace Emby.Server.Implementations.Dto
// .ToList();
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
// .Except(foundArtists, new DistinctNameComparer())
//.Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
// This should not be necessary but we're seeing some cases of it
@@ -1026,7 +1035,8 @@ namespace Emby.Server.Implementations.Dto
}
// Add video info
if (item is Video video)
var video = item as Video;
if (video != null)
{
dto.VideoType = video.VideoType;
dto.Video3DFormat = video.Video3DFormat;
@@ -1065,7 +1075,9 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaStreams))
{
// Add VideoInfo
if (item is IHasMediaSources)
var iHasMediaSources = item as IHasMediaSources;
if (iHasMediaSources != null)
{
MediaStream[] mediaStreams;
@@ -1134,7 +1146,7 @@ namespace Emby.Server.Implementations.Dto
// TODO maybe remove the if statement entirely
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
episodeSeries ??= episode.Series;
episodeSeries = episodeSeries ?? episode.Series;
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
@@ -1147,7 +1159,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
episodeSeries ??= episode.Series;
episodeSeries = episodeSeries ?? episode.Series;
if (episodeSeries != null)
{
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
@@ -1160,7 +1172,7 @@ namespace Emby.Server.Implementations.Dto
{
dto.AirDays = series.AirDays;
dto.AirTime = series.AirTime;
dto.Status = series.Status?.ToString();
dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
}
// Add SeasonInfo
@@ -1173,7 +1185,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
series ??= season.Series;
series = series ?? season.Series;
if (series != null)
{
dto.SeriesStudio = series.Studios.FirstOrDefault();
@@ -1184,7 +1196,7 @@ namespace Emby.Server.Implementations.Dto
// TODO maybe remove the if statement entirely
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
series ??= season.Series;
series = series ?? season.Series;
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
@@ -1271,7 +1283,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 == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
{
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
}
@@ -1304,12 +1316,9 @@ 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)
|| parent is Series)
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) || parent is Series) &&
(parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
{
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
if (parent == null)
{
break;
@@ -1339,7 +1348,7 @@ namespace Emby.Server.Implementations.Dto
}
}
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
@@ -1389,6 +1398,7 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
{
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
@@ -1403,27 +1413,44 @@ namespace Emby.Server.Implementations.Dto
return null;
}
ImageDimensions size;
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
if (defaultAspectRatio > 0)
{
return defaultAspectRatio;
}
if (!imageInfo.IsLocalFile)
{
return item.GetDefaultPrimaryImageAspectRatio();
return null;
}
try
{
var size = _imageProcessor.GetImageDimensions(item, imageInfo);
var width = size.Width;
var height = size.Height;
if (width > 0 && height > 0)
size = _imageProcessor.GetImageDimensions(item, imageInfo);
if (size.Width <= 0 || size.Height <= 0)
{
return (double)width / height;
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
return null;
}
return item.GetDefaultPrimaryImageAspectRatio();
var width = size.Width;
var height = size.Height;
if (width <= 0 || height <= 0)
{
return null;
}
return (double)width / height;
}
}
}

View File

@@ -23,18 +23,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
<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.1" />
<PackageReference Include="Mono.Nat" Version="3.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
<PackageReference Include="sharpcompress" Version="0.30.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
<PackageReference Include="sharpcompress" Version="0.28.3" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
@@ -42,21 +41,22 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>

View File

@@ -9,10 +9,12 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Dlna;
using Microsoft.Extensions.Logging;
using Mono.Nat;
@@ -26,6 +28,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IServerApplicationHost _appHost;
private readonly ILogger<ExternalPortForwarding> _logger;
private readonly IServerConfigurationManager _config;
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
@@ -40,14 +43,17 @@ namespace Emby.Server.Implementations.EntryPoints
/// <param name="logger">The logger.</param>
/// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param>
/// <param name="deviceDiscovery">The device discovery.</param>
public ExternalPortForwarding(
ILogger<ExternalPortForwarding> logger,
IServerApplicationHost appHost,
IServerConfigurationManager config)
IServerConfigurationManager config,
IDeviceDiscovery deviceDiscovery)
{
_logger = logger;
_appHost = appHost;
_config = config;
_deviceDiscovery = deviceDiscovery;
}
private string GetConfigIdentifier()

View File

@@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
_lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
_lastProgressMessageTimes.AddOrUpdate(item.Id, key => DateTime.UtcNow, (key, existing) => DateTime.UtcNow);
var dict = new Dictionary<string, string>();
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
@@ -144,12 +144,12 @@ namespace Emby.Server.Implementations.EntryPoints
{
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out DateTime removed);
}
private static bool EnableRefreshMessage(BaseItem item)
{
if (item is not Folder folder)
if (!(item is Folder folder))
{
return false;
}
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
return false;
}
if (item is IItemByName && item is not MusicArtist)
if (item is IItemByName && !(item is MusicArtist))
{
return false;
}
@@ -423,6 +423,7 @@ namespace Emby.Server.Implementations.EntryPoints
continue;
}
var collectionFolders = _libraryManager.GetCollectionFolders(item, allUserRootChildren);
foreach (var folder in allUserRootChildren)
{
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
@@ -435,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Translates the physical item to user library.
/// </summary>
/// <typeparam name="T">The type of item.</typeparam>
/// <typeparam name="T"></typeparam>
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
@@ -464,7 +465,6 @@ namespace Emby.Server.Implementations.EntryPoints
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>

View File

@@ -37,9 +37,6 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
public UdpServerEntryPoint(
ILogger<UdpServerEntryPoint> logger,
IServerApplicationHost appHost,

View File

@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.EntryPoints
var changes = _changedItems.ToList();
_changedItems.Clear();
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
var task = SendNotifications(changes, CancellationToken.None);
if (_updateTimer != null)
{

View File

@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -17,13 +17,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
public AuthorizationInfo Authenticate(HttpRequest request)
{
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!auth.HasToken)
{
return auth;
throw new AuthenticationException("Request does not contain a token.");
}
if (!auth.IsAuthenticated)

View File

@@ -3,40 +3,40 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Server.Implementations.Security
namespace Emby.Server.Implementations.HttpServer.Security
{
public class AuthorizationContext : IAuthorizationContext
{
private readonly JellyfinDbProvider _jellyfinDbProvider;
private readonly IAuthenticationRepository _authRepo;
private readonly IUserManager _userManager;
public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
{
_jellyfinDbProvider = jellyfinDb;
_authRepo = authRepo;
_userManager = userManager;
}
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return Task.FromResult((AuthorizationInfo)cached); // Cache should never contain null
return (AuthorizationInfo)cached!; // Cache should never contain null
}
return GetAuthorization(requestContext);
}
public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false);
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
@@ -45,22 +45,22 @@ namespace Jellyfin.Server.Implementations.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
IReadOnlyDictionary<string, string>? auth,
IHeaderDictionary headers,
IQueryCollection queryString)
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string>? auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
string? deviceId = null;
string? deviceName = null;
string? device = null;
string? client = null;
string? version = null;
string? token = null;
@@ -68,13 +68,12 @@ namespace Jellyfin.Server.Implementations.Security
if (auth != null)
{
auth.TryGetValue("DeviceId", out deviceId);
auth.TryGetValue("Device", out deviceName);
auth.TryGetValue("Device", out device);
auth.TryGetValue("Client", out client);
auth.TryGetValue("Version", out version);
auth.TryGetValue("Token", out token);
}
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
if (string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
@@ -99,7 +98,7 @@ namespace Jellyfin.Server.Implementations.Security
var authInfo = new AuthorizationInfo
{
Client = client,
Device = deviceName,
Device = device,
DeviceId = deviceId,
Version = version,
Token = token,
@@ -112,95 +111,90 @@ namespace Jellyfin.Server.Implementations.Security
// Request doesn't contain a token.
return authInfo;
}
#pragma warning restore CA1508
authInfo.HasToken = true;
await using var dbContext = _jellyfinDbProvider.CreateContext();
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
if (device != null)
if (result.Items.Count > 0)
{
authInfo.IsAuthenticated = true;
}
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = device.AppName;
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
authInfo.DeviceId = device.DeviceId;
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
var allowTokenInfoUpdate = authInfo.Client == null || !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
authInfo.Device = device.DeviceName;
authInfo.Device = originalAuthenticationInfo.DeviceName;
}
else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
device.DeviceName = authInfo.Device;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = device.AppVersion;
authInfo.Version = originalAuthenticationInfo.AppVersion;
}
else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
device.AppVersion = authInfo.Version;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
device.DateLastActivity = DateTime.UtcNow;
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
authInfo.User = _userManager.GetUserById(device.UserId);
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
authInfo.IsApiKey = false;
}
else
{
authInfo.IsApiKey = true;
}
if (updateToken)
{
dbContext.Devices.Update(device);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
else
{
var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
if (key != null)
{
authInfo.IsAuthenticated = true;
authInfo.Client = key.Name;
authInfo.Token = key.AccessToken;
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
authInfo.DeviceId = string.Empty;
}
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
authInfo.Device = string.Empty;
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = string.Empty;
}
authInfo.IsApiKey = true;
_authRepo.Update(originalAuthenticationInfo);
}
}
@@ -212,7 +206,7 @@ namespace Jellyfin.Server.Implementations.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@@ -221,7 +215,7 @@ namespace Jellyfin.Server.Implementations.Security
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
@@ -229,7 +223,7 @@ namespace Jellyfin.Server.Implementations.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@@ -238,7 +232,7 @@ namespace Jellyfin.Server.Implementations.Security
auth = httpReq.Headers[HeaderNames.Authorization];
}
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
@@ -246,8 +240,13 @@ namespace Jellyfin.Server.Implementations.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
if (authorizationHeader == null)
{
return null;
}
var firstSpace = authorizationHeader.IndexOf(' ');
// There should be at least two parts
@@ -264,57 +263,29 @@ namespace Jellyfin.Server.Implementations.Security
return null;
}
// Remove up until the first space
authorizationHeader = authorizationHeader[(firstSpace + 1)..];
return GetParts(authorizationHeader);
}
/// <summary>
/// Get the authorization header components.
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
public static Dictionary<string, string> GetParts(ReadOnlySpan<char> authorizationHeader)
{
var result = new Dictionary<string, string>();
var escaped = false;
int start = 0;
string key = string.Empty;
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
int i;
for (i = 0; i < authorizationHeader.Length; i++)
foreach (var item in authorizationHeader.Split(','))
{
var token = authorizationHeader[i];
if (token == '"' || token == ',')
{
// Applying a XOR logic to evaluate whether it is opening or closing a value
escaped = (!escaped) == (token == '"');
if (token == ',' && !escaped)
{
// Meeting a comma after a closing escape char means the value is complete
if (start < i)
{
result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
key = string.Empty;
}
var trimmedItem = item.Trim();
var firstEqualsSign = trimmedItem.IndexOf('=');
start = i + 1;
}
}
else if (!escaped && token == '=')
if (firstEqualsSign > 0)
{
key = authorizationHeader[start.. i].Trim().ToString();
start = i + 1;
var key = trimmedItem[..firstEqualsSign].ToString();
var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
result[key] = value;
}
}
// Add last value
if (start < i)
{
result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
}
return result;
}
private static string NormalizeValue(string value)
{
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}

View File

@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
@@ -24,33 +23,27 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
public async Task<SessionInfo> GetSession(HttpContext requestContext)
public SessionInfo GetSession(HttpContext requestContext)
{
var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
return await _sessionManager.LogSessionActivity(
authorization.Client,
authorization.Version,
authorization.DeviceId,
authorization.Device,
requestContext.GetNormalizedRemoteIp().ToString(),
user).ConfigureAwait(false);
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
}
public Task<SessionInfo> GetSession(object requestContext)
public SessionInfo GetSession(object requestContext)
{
return GetSession((HttpContext)requestContext);
}
public async Task<User?> GetUser(HttpContext requestContext)
public User? GetUser(HttpContext requestContext)
{
var session = await GetSession(requestContext).ConfigureAwait(false);
var session = GetSession(requestContext);
return session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
public Task<User?> GetUser(object requestContext)
public User? GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}

View File

@@ -42,14 +42,17 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="query">The query.</param>
public WebSocketConnection(
ILogger<WebSocketConnection> logger,
WebSocket socket,
IPAddress? remoteEndPoint)
IPAddress? remoteEndPoint,
IQueryCollection query)
{
_logger = logger;
_socket = socket;
RemoteEndPoint = remoteEndPoint;
QueryString = query;
_jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now;
@@ -59,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets the remote end point.
/// Gets or sets the remote end point.
/// </summary>
public IPAddress? RemoteEndPoint { get; }
@@ -78,6 +81,12 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public DateTime LastKeepAliveDate { get; set; }
/// <summary>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
public IQueryCollection QueryString { get; }
/// <summary>
/// Gets the state.
/// </summary>
@@ -87,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Sends a message asynchronously.
/// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <typeparam name="T"></typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -141,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
}
while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
} while (
(_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
@@ -171,7 +180,7 @@ namespace Emby.Server.Implementations.HttpServer
}
WebSocketMessage<object>? stub;
long bytesConsumed;
long bytesConsumed = 0;
try
{
stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
@@ -227,8 +236,7 @@ namespace Emby.Server.Implementations.HttpServer
{
MessageId = Guid.NewGuid(),
MessageType = SessionMessageType.KeepAlive
},
CancellationToken.None);
}, CancellationToken.None);
}
/// <inheritdoc />

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -36,12 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
if (!authorizationInfo.IsAuthenticated)
{
throw new SecurityException("Token is required");
}
_ = _authService.Authenticate(context.Request);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
@@ -51,7 +45,8 @@ namespace Emby.Server.Implementations.HttpServer
using var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
context.GetNormalizedRemoteIp())
context.Connection.RemoteIpAddress,
context.Request.Query)
{
OnReceive = ProcessWebSocketMessageReceived
};
@@ -59,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
{
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection);
}
await Task.WhenAll(tasks).ConfigureAwait(false);

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -12,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
public sealed class FileRefresher : IDisposable
public class FileRefresher : IDisposable
{
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
@@ -20,7 +22,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object();
private Timer? _timer;
private Timer _timer;
private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
@@ -34,7 +36,7 @@ namespace Emby.Server.Implementations.IO
AddPath(path);
}
public event EventHandler<EventArgs>? Completed;
public event EventHandler<EventArgs> Completed;
public string Path { get; private set; }
@@ -109,7 +111,7 @@ namespace Emby.Server.Implementations.IO
RestartTimer();
}
private void OnTimerCallback(object? state)
private void OnTimerCallback(object state)
{
List<string> paths;
@@ -125,7 +127,7 @@ namespace Emby.Server.Implementations.IO
try
{
ProcessPathChanges(paths);
ProcessPathChanges(paths.ToList());
}
catch (Exception ex)
{
@@ -135,12 +137,12 @@ namespace Emby.Server.Implementations.IO
private void ProcessPathChanges(List<string> paths)
{
IEnumerable<BaseItem> itemsToRefresh = paths
var itemsToRefresh = paths
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAffectedBaseItem)
.Where(item => item != null)
.GroupBy(x => x!.Id) // Removed null values in the previous .Where()
.Select(x => x.First())!;
.GroupBy(x => x.Id)
.Select(x => x.First());
foreach (var item in itemsToRefresh)
{
@@ -174,15 +176,15 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BaseItem.</returns>
private BaseItem? GetAffectedBaseItem(string path)
private BaseItem GetAffectedBaseItem(string path)
{
BaseItem? item = null;
BaseItem item = null;
while (item == null && !string.IsNullOrEmpty(path))
{
item = _libraryManager.FindByPath(path, null);
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
path = System.IO.Path.GetDirectoryName(path);
}
if (item != null)
@@ -217,13 +219,8 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
DisposeTimer();
_disposed = true;
DisposeTimer();
GC.SuppressFinalize(this);
}
}

View File

@@ -41,25 +41,6 @@ namespace Emby.Server.Implementations.IO
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
}
/// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary>
@@ -99,7 +80,7 @@ namespace Emby.Server.Implementations.IO
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
await Task.Delay(45000).ConfigureAwait(false);
_tempIgnoredPaths.TryRemove(path, out _);
_tempIgnoredPaths.TryRemove(path, out var val);
if (refreshPath)
{
@@ -114,6 +95,21 @@ namespace Emby.Server.Implementations.IO
}
}
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// </summary>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
}
private bool IsLibraryMonitorEnabled(BaseItem item)
{
if (item is BasePluginFolder)
@@ -203,7 +199,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="lst">The LST.</param>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">path</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
{
if (string.IsNullOrEmpty(path))
@@ -267,7 +263,7 @@ namespace Emby.Server.Implementations.IO
if (_fileSystemWatchers.TryAdd(path, newWatcher))
{
newWatcher.EnableRaisingEvents = true;
_logger.LogInformation("Watching directory {Path}", path);
_logger.LogInformation("Watching directory " + path);
}
else
{
@@ -276,7 +272,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
_logger.LogError(ex, "Error watching path: {Path}", path);
_logger.LogError(ex, "Error watching path: {path}", path);
}
});
}
@@ -449,12 +445,12 @@ namespace Emby.Server.Implementations.IO
}
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
newRefresher.Completed += OnNewRefresherCompleted;
newRefresher.Completed += NewRefresher_Completed;
_activeRefreshers.Add(newRefresher);
}
}
private void OnNewRefresherCompleted(object sender, EventArgs e)
private void NewRefresher_Completed(object sender, EventArgs e)
{
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
@@ -481,7 +477,6 @@ namespace Emby.Server.Implementations.IO
{
lock (_activeRefreshers)
{
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
_activeRefreshers.Remove(refresher);
}
@@ -493,7 +488,6 @@ namespace Emby.Server.Implementations.IO
{
foreach (var refresher in _activeRefreshers.ToList())
{
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
}

View File

@@ -1,11 +1,15 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
@@ -15,26 +19,20 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
private readonly ILogger<ManagedFileSystem> _logger;
protected ILogger<ManagedFileSystem> Logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths)
{
_logger = logger;
Logger = logger;
_tempPath = applicationPaths.TempDirectory;
}
/// <inheritdoc />
public virtual void AddShortcutHandler(IShortcutHandler handler)
{
_shortcutHandlers.Add(handler);
@@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">filename</exception>
public virtual bool IsShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -62,7 +60,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">filename</exception>
public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -76,7 +74,6 @@ namespace Emby.Server.Implementations.IO
return handler?.Resolve(filename);
}
/// <inheritdoc />
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
// path is actually a stream
@@ -238,9 +235,9 @@ namespace Emby.Server.Implementations.IO
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
// {
//{
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
// }
//}
if (info is FileInfo fileInfo)
{
@@ -251,15 +248,15 @@ namespace Emby.Server.Implementations.IO
{
try
{
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{
result.Length = RandomAccess.GetLength(fileHandle);
result.Length = thisFileStream.Length;
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
@@ -348,7 +345,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
_logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
Logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -363,13 +360,11 @@ namespace Emby.Server.Implementations.IO
return GetCreationTimeUtc(GetFileSystemInfo(path));
}
/// <inheritdoc />
public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info)
{
return info.CreationTimeUtc;
}
/// <inheritdoc />
public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
{
return info.LastWriteTimeUtc;
@@ -389,7 +384,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
_logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
Logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -404,7 +399,6 @@ namespace Emby.Server.Implementations.IO
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
}
/// <inheritdoc />
public virtual void SetHidden(string path, bool isHidden)
{
if (!OperatingSystem.IsWindows())
@@ -429,8 +423,7 @@ namespace Emby.Server.Implementations.IO
}
}
/// <inheritdoc />
public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
{
if (!OperatingSystem.IsWindows())
{
@@ -444,16 +437,16 @@ namespace Emby.Server.Implementations.IO
return;
}
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
if (info.IsReadOnly == isReadOnly && info.IsHidden == isHidden)
{
return;
}
var attributes = File.GetAttributes(path);
if (readOnly)
if (isReadOnly)
{
attributes |= FileAttributes.ReadOnly;
attributes = attributes | FileAttributes.ReadOnly;
}
else
{
@@ -462,7 +455,7 @@ namespace Emby.Server.Implementations.IO
if (isHidden)
{
attributes |= FileAttributes.Hidden;
attributes = attributes | FileAttributes.Hidden;
}
else
{
@@ -507,7 +500,6 @@ namespace Emby.Server.Implementations.IO
File.Copy(temp1, file2, true);
}
/// <inheritdoc />
public virtual bool ContainsSubPath(string parentPath, string path)
{
if (string.IsNullOrEmpty(parentPath))
@@ -525,7 +517,6 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
/// <inheritdoc />
public virtual string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
@@ -541,16 +532,24 @@ namespace Emby.Server.Implementations.IO
return Path.TrimEndingDirectorySeparator(path);
}
/// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
if (path1 == null && path2 == null)
{
return true;
}
if (path1 == null || path2 == null)
{
return false;
}
return string.Equals(
NormalizePath(path1),
NormalizePath(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
/// <inheritdoc />
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
{
if (info.IsDirectory)
@@ -561,11 +560,11 @@ namespace Emby.Server.Implementations.IO
return Path.GetFileNameWithoutExtension(info.FullName);
}
/// <inheritdoc />
public virtual bool IsPathFile(string path)
{
if (path.Contains("://", StringComparison.OrdinalIgnoreCase)
&& !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
// Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
!path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -573,23 +572,17 @@ namespace Emby.Server.Implementations.IO
return true;
}
/// <inheritdoc />
public virtual void DeleteFile(string path)
{
SetAttributes(path, false, false);
File.Delete(path);
}
/// <inheritdoc />
public virtual List<FileSystemMetadata> GetDrives()
{
// check for ready state to avoid waiting for drives to timeout
// some drives on linux have no actual size or are used for other purposes
return DriveInfo.GetDrives()
.Where(
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
&& d.IsReady
&& d.TotalSize != 0)
return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram)
.Select(d => new FileSystemMetadata
{
Name = d.Name,
@@ -598,19 +591,16 @@ namespace Emby.Server.Implementations.IO
}).ToList();
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{
return GetFiles(path, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -641,7 +631,6 @@ namespace Emby.Server.Implementations.IO
return ToMetadata(files);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
var directoryInfo = new DirectoryInfo(path);
@@ -655,19 +644,16 @@ namespace Emby.Server.Implementations.IO
return infos.Select(GetFileSystemMetadata);
}
/// <inheritdoc />
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
/// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
{
return GetFilePaths(path, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -698,7 +684,6 @@ namespace Emby.Server.Implementations.IO
return files;
}
/// <inheritdoc />
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));

View File

@@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
try
{
int read;
while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
if (onStarted != null)
{
@@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
if (emptyReadLimit <= 0)
{
int read;
while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
}
return;
@@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
{
cancellationToken.ThrowIfCancellationRequested();
var bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
if (bytesRead == 0)
{
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
{
eofCount = 0;
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -88,13 +88,13 @@ namespace Emby.Server.Implementations.IO
{
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
var bytesToWrite = Math.Min(bytesRead, copyLength);
if (bytesToWrite > 0)
{
await destination.WriteAsync(buffer.AsMemory(0, Convert.ToInt32(bytesToWrite)), cancellationToken).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
}
copyLength -= bytesToWrite;
@@ -137,9 +137,9 @@ namespace Emby.Server.Implementations.IO
int bytesRead;
int totalBytesRead = 0;
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
}

View File

@@ -1,8 +1,7 @@
#pragma warning disable CS1591
namespace Emby.Server.Implementations
{
/// <summary>
/// Specifies the contract for server startup options.
/// </summary>
public interface IStartupOptions
{
/// <summary>
@@ -11,7 +10,7 @@ namespace Emby.Server.Implementations
string? FFmpegPath { get; }
/// <summary>
/// Gets a value indicating whether to run as service by the --service command line option.
/// Gets the value of the --service command line option.
/// </summary>
bool IsService { get; }

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