Merge remote-tracking branch 'upstream/master' into schedules-direct

This commit is contained in:
Cody Robibero 2021-10-08 07:49:40 -06:00
commit 3bbd98cc3f
452 changed files with 6467 additions and 4633 deletions

View File

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest" default: "ubuntu-latest"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 5.0.302 default: 6.0.x
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck
@ -34,6 +34,7 @@ jobs:
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker Tool' displayName: 'Install ABI CompatibilityChecker Tool'

View File

@ -1,7 +1,7 @@
parameters: parameters:
LinuxImage: 'ubuntu-latest' LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 5.0.302 DotNetSdkVersion: 6.0.x
jobs: jobs:
- job: Build - job: Build
@ -54,6 +54,7 @@ jobs:
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Publish Server' displayName: 'Publish Server'
@ -91,3 +92,10 @@ jobs:
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll' targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common' 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

@ -195,10 +195,11 @@ jobs:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET 5.0 sdk' displayName: 'Use .NET 6.0 sdk'
inputs: inputs:
packageType: 'sdk' packageType: 'sdk'
version: '5.0.x' version: '6.0.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages' displayName: 'Build Stable Nuget packages'
@ -211,6 +212,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj Emby.Naming/Emby.Naming.csproj
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack' custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion) arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
@ -225,6 +227,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj Emby.Naming/Emby.Naming.csproj
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack' custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable' arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'

View File

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj" default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 5.0.302 default: 6.0.x
jobs: jobs:
- job: Test - job: Test
@ -41,6 +41,7 @@ jobs:
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
includePreviewVersions: true
- task: SonarCloudPrepare@1 - task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud' displayName: 'Prepare analysis on SonarCloud'
@ -94,5 +95,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact' displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs: inputs:
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json" targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
artifactName: 'OpenAPI Spec' artifactName: 'OpenAPI Spec'

View File

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

View File

@ -14,8 +14,9 @@ assignees: ''
- OS: [e.g. Debian, Windows] - OS: [e.g. Debian, Windows]
- Virtualization: [e.g. Docker, KVM, LXC] - Virtualization: [e.g. Docker, KVM, LXC]
- Clients: [Browser, Android, Fire Stick, etc.] - Clients: [Browser, Android, Fire Stick, etc.]
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13] - Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231] - Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
- FFmpeg Version: [e.g. 4.3.2-Jellyfin]
- Playback: [Direct Play, Remux, Direct Stream, Transcode] - Playback: [Direct Play, Remux, Direct Stream, Transcode]
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.] - Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.] - Installed Plugins: [e.g. none, Fanart, Anime, etc.]

View File

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

View File

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

4
.vscode/launch.json vendored
View File

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

View File

@ -46,6 +46,7 @@
- [fruhnow](https://github.com/fruhnow) - [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername) - [geilername](https://github.com/geilername)
- [gnattu](https://github.com/gnattu) - [gnattu](https://github.com/gnattu)
- [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero) - [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk) - [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93) - [hawken93](https://github.com/hawken93)

View File

@ -1,4 +1,8 @@
ARG DOTNET_VERSION=5.0 # 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
FROM node:lts-alpine as web-builder FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
@ -8,7 +12,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && mv dist /dist
FROM debian:bullseye-slim as app FROM debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@ -18,10 +22,10 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
# https://github.com/intel/compute-runtime/releases # https://github.com/intel/compute-runtime/releases
ARG GMMLIB_VERSION=20.3.2 ARG GMMLIB_VERSION=21.2.1
ARG IGC_VERSION=1.0.5435 ARG IGC_VERSION=1.0.8517
ARG NEO_VERSION=20.46.18421 ARG NEO_VERSION=21.35.20826
ARG LEVEL_ZERO_VERSION=1.0.18421 ARG LEVEL_ZERO_VERSION=1.2.20826
# Install dependencies: # Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding. # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
@ -57,7 +61,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \ && 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 && 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 LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en ENV LANGUAGE en_US:en

View File

@ -1,8 +1,8 @@
# DESIGNED FOR BUILDING ON AMD64 ONLY # DESIGNED FOR BUILDING ON ARM ONLY
##################################### #####################################
# Requires binfm_misc registration # Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=5.0 ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder 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 && mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu FROM multiarch/qemu-user-static:x86_64-arm as qemu
FROM arm32v7/debian:bullseye-slim as app FROM arm32v7/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@ -50,7 +50,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \ && 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 && 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 LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en ENV LANGUAGE en_US:en

View File

@ -1,8 +1,8 @@
# DESIGNED FOR BUILDING ON AMD64 ONLY # DESIGNED FOR BUILDING ON ARM64 ONLY
##################################### #####################################
# Requires binfm_misc registration # Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=5.0 ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder 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 && mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:bullseye-slim as app FROM arm64v8/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@ -40,7 +40,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \ && 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 && 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 LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en ENV LANGUAGE en_US:en

View File

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

View File

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

View File

@ -41,8 +41,6 @@ namespace Emby.Dlna.Didl
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; 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 DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress; private readonly string _serverAddress;
@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue) if (mediaSource.RunTimeTicks.HasValue)
{ {
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
} }
if (filter.Contains("res@size")) if (filter.Contains("res@size"))
@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue) if (size.HasValue)
{ {
writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
} }
} }
} }
@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue) if (targetChannels.HasValue)
{ {
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
} }
if (filter.Contains("res@resolution")) if (filter.Contains("res@resolution"))
@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl
if (targetSampleRate.HasValue) if (targetSampleRate.HasValue)
{ {
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
} }
if (totalBitrate.HasValue) if (totalBitrate.HasValue)
{ {
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
} }
var mediaProfile = _profile.GetVideoMediaProfile( var mediaProfile = _profile.GetVideoMediaProfile(
@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue) if (mediaSource.RunTimeTicks.HasValue)
{ {
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
} }
if (filter.Contains("res@size")) if (filter.Contains("res@size"))
@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue) if (size.HasValue)
{ {
writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
} }
} }
} }
@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue) if (targetChannels.HasValue)
{ {
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
} }
if (targetSampleRate.HasValue) if (targetSampleRate.HasValue)
{ {
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
} }
if (targetAudioBitrate.HasValue) if (targetAudioBitrate.HasValue)
{ {
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
} }
var mediaProfile = _profile.GetAudioMediaProfile( var mediaProfile = _profile.GetAudioMediaProfile(
@ -639,7 +637,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1"); writer.WriteAttributeString("searchable", "1");
writer.WriteAttributeString("childCount", childCount.ToString(_usCulture)); writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
var clientId = GetClientId(folder, stubType); var clientId = GetClientId(folder, stubType);
@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl
if (item.IndexNumber.HasValue) if (item.IndexNumber.HasValue)
{ {
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp); AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
if (item is Episode) if (item is Episode)
{ {
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp); AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
} }
} }
} }

View File

@ -366,7 +366,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(systemProfilesPath); Directory.CreateDirectory(systemProfilesPath);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{ {
await stream.CopyToAsync(fileStream).ConfigureAwait(false); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }
@ -486,18 +486,22 @@ namespace Emby.Dlna
} }
/// <inheritdoc /> /// <inheritdoc />
public ImageStream GetIcon(string filename) public ImageStream? GetIcon(string filename)
{ {
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase) var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png ? ImageFormat.Png
: ImageFormat.Jpg; : ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant(); var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
var stream = _assembly.GetManifestResourceStream(resource);
return new ImageStream if (stream == null)
{ {
Format = format, return null;
Stream = _assembly.GetManifestResourceStream(resource) }
return new ImageStream(stream)
{
Format = format
}; };
} }

View File

@ -17,10 +17,9 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View File

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

View File

@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class Device : IDisposable public class Device : IDisposable
{ {
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
Volume = int.Parse(volumeValue, UsCulture); Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
if (Volume > 0) if (Volume > 0)
{ {
@ -842,7 +840,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(duration) if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{ {
Duration = TimeSpan.Parse(duration, UsCulture); Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
} }
else else
{ {
@ -854,7 +852,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{ {
Position = TimeSpan.Parse(position, UsCulture); Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
} }
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault(); var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@ -1194,8 +1192,8 @@ namespace Emby.Dlna.PlayTo
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")); var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")); var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture); var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture); var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
return new DeviceIcon return new DeviceIcon
{ {

View File

@ -30,8 +30,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlayToController : ISessionController, IDisposable public class PlayToController : ISessionController, IDisposable
{ {
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private readonly SessionInfo _session; private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -716,7 +714,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetAudioStreamIndex: case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index)) if (command.Arguments.TryGetValue("Index", out string index))
{ {
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val)) if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{ {
return SetAudioStreamIndex(val); return SetAudioStreamIndex(val);
} }
@ -728,7 +726,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetSubtitleStreamIndex: case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index)) if (command.Arguments.TryGetValue("Index", out index))
{ {
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val)) if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{ {
return SetSubtitleStreamIndex(val); return SetSubtitleStreamIndex(val);
} }
@ -740,7 +738,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetVolume: case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol)) if (command.Arguments.TryGetValue("Volume", out string vol))
{ {
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume)) if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{ {
return _device.SetVolume(volume, cancellationToken); return _device.SetVolume(volume, cancellationToken);
} }

View File

@ -173,7 +173,9 @@ namespace Emby.Dlna.PlayTo
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture); uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
} }
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null); var sessionInfo = await _sessionManager
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
.ConfigureAwait(false);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault(); var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();

View File

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

View File

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

View File

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

View File

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

View File

@ -102,7 +102,7 @@ namespace Emby.Drawing
{ {
var file = await ProcessImage(options).ConfigureAwait(false); var file = await ProcessImage(options).ConfigureAwait(false);
using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true)) using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{ {
await fileStream.CopyToAsync(toStream).ConfigureAwait(false); await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
} }

View File

@ -478,6 +478,12 @@ namespace Emby.Naming.Common
"-deleted", "-deleted",
MediaType.Video), MediaType.Video),
new ExtraRule(
ExtraType.DeletedScene,
ExtraRuleType.Suffix,
"-deletedscene",
MediaType.Video),
new ExtraRule( new ExtraRule(
ExtraType.Clip, ExtraType.Clip,
ExtraRuleType.Suffix, ExtraRuleType.Suffix,

View File

@ -6,14 +6,13 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'"> <PropertyGroup Condition=" '$(Stability)'=='Unstable'">

View File

@ -11,6 +11,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
public class ExtraResolver public class ExtraResolver
{ {
private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private readonly NamingOptions _options; private readonly NamingOptions _options;
/// <summary> /// <summary>
@ -62,9 +63,10 @@ namespace Emby.Naming.Video
} }
else if (rule.RuleType == ExtraRuleType.Suffix) else if (rule.RuleType == ExtraRuleType.Suffix)
{ {
var filename = Path.GetFileNameWithoutExtension(pathSpan); // Trim the digits from the end of the filename so we can recognize things like -trailer2
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase)) if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{ {
result.ExtraType = rule.ExtraType; result.ExtraType = rule.ExtraType;
result.Rule = rule; result.Rule = rule;

View File

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

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

View File

@ -38,7 +38,6 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins; using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.SyncPlay;
@ -59,7 +58,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -75,7 +73,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.Subtitles;
@ -117,6 +114,11 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
/// <summary>
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig; private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
@ -128,104 +130,15 @@ namespace Emby.Server.Implementations
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private string[] _urlPrefixes; private string[] _urlPrefixes;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
{
if (!Environment.UserInteractive)
{
return false;
}
if (_startupOptions.IsService)
{
return false;
}
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
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.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; }
/// <inheritdoc />
public bool IsShuttingDown { get; private set; }
/// <summary>
/// Gets the logger.
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
/// <summary>
/// Gets the logger factory.
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
/// </summary> /// </summary>
/// <value>All concrete types.</value> /// <value>All concrete types.</value>
private Type[] _allConcreteTypes; private Type[] _allConcreteTypes;
/// <summary> private DeviceId _deviceId;
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
/// <summary> private bool _disposed = false;
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
public IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// Gets the http port for the webhost.
/// </summary>
public int HttpPort { get; private set; }
/// <summary>
/// Gets the https port for the webhost.
/// </summary>
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -268,6 +181,143 @@ namespace Emby.Server.Implementations
ApplicationVersion); ApplicationVersion);
} }
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
{
if (!Environment.UserInteractive)
{
return false;
}
if (_startupOptions.IsService)
{
return false;
}
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; internal set; }
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; }
/// <inheritdoc />
public bool IsShuttingDown { get; private set; }
/// <summary>
/// Gets the logger.
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
/// <summary>
/// Gets the logger factory.
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
public IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// Gets the http port for the webhost.
/// </summary>
public int HttpPort { get; private set; }
/// <summary>
/// Gets the https port for the webhost.
/// </summary>
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <inheritdoc />
public Version ApplicationVersion { get; }
/// <inheritdoc />
public string ApplicationVersionString { get; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
public string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
/// </summary>
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
public string SystemId
{
get
{
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
}
/// <inheritdoc/>
public string Name => ApplicationProductName;
private string CertificatePath { get; set; }
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;
/// <summary> /// <summary>
/// Temporary function to migration network settings out of system.xml and into network.xml. /// 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. /// TODO: remove at the point when a fixed migration path has been decided upon.
@ -300,45 +350,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
} }
/// <inheritdoc />
public Version ApplicationVersion { get; }
/// <inheritdoc />
public string ApplicationVersionString { get; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
public string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
/// </summary>
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
private DeviceId _deviceId;
public string SystemId
{
get
{
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
}
/// <inheritdoc/>
public string Name => ApplicationProductName;
/// <summary> /// <summary>
/// Creates an instance of type and resolves all constructor dependencies. /// Creates an instance of type and resolves all constructor dependencies.
/// </summary> /// </summary>
@ -537,12 +548,8 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort; HttpsPort = NetworkConfiguration.DefaultHttpsPort;
} }
CertificateInfo = new CertificateInfo CertificatePath = networkConfiguration.CertificatePath;
{ Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
RegisterServices(); RegisterServices();
@ -595,8 +602,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>(); ServiceCollection.AddSingleton<EncodingHelper>();
@ -618,8 +623,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
@ -655,8 +658,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); ServiceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>(); ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
@ -685,8 +687,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>(); _mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>(); _sessionManager = Resolve<ISessionManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties(); SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
@ -725,30 +725,27 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
} }
private X509Certificate2 GetCertificate(CertificateInfo info) private X509Certificate2 GetCertificate(string path, string password)
{ {
var certificateLocation = info?.Path; if (string.IsNullOrWhiteSpace(path))
if (string.IsNullOrWhiteSpace(certificateLocation))
{ {
return null; return null;
} }
try try
{ {
if (!File.Exists(certificateLocation)) if (!File.Exists(path))
{ {
return null; return null;
} }
// Don't use an empty string password // Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; password = string.IsNullOrWhiteSpace(password) ? null : password;
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet); var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey) if (!localCert.HasPrivateKey)
{ {
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation); Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
return null; return null;
} }
@ -756,7 +753,7 @@ namespace Emby.Server.Implementations
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation); Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
return null; return null;
} }
} }
@ -867,10 +864,6 @@ namespace Emby.Server.Implementations
} }
} }
private CertificateInfo CertificateInfo { get; set; }
public X509Certificate2 Certificate { get; private set; }
private IEnumerable<string> GetUrlPrefixes() private IEnumerable<string> GetUrlPrefixes()
{ {
var hosts = new[] { "+" }; var hosts = new[] { "+" };
@ -882,7 +875,7 @@ namespace Emby.Server.Implementations
"http://" + i + ":" + HttpPort + "/" "http://" + i + ":" + HttpPort + "/"
}; };
if (CertificateInfo != null) if (Certificate != null)
{ {
prefixes.Add("https://" + i + ":" + HttpsPort + "/"); prefixes.Add("https://" + i + ":" + HttpsPort + "/");
} }
@ -946,7 +939,7 @@ namespace Emby.Server.Implementations
var newPath = networkConfig.CertificatePath; var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath) if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal)) && !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
{ {
if (File.Exists(newPath)) if (File.Exists(newPath))
{ {
@ -1124,9 +1117,6 @@ namespace Emby.Server.Implementations
}; };
} }
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null) public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
{ {
@ -1213,14 +1203,7 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/'); }.ToString().TrimEnd('/');
} }
public string FriendlyName => /// <inheritdoc />
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Shuts down.
/// </summary>
public async Task Shutdown() public async Task Shutdown()
{ {
if (IsShuttingDown) if (IsShuttingDown)
@ -1258,41 +1241,7 @@ namespace Emby.Server.Implementations
} }
} }
public virtual void LaunchUrl(string url) /// <inheritdoc />
{
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() public void Dispose()
{ {
Dispose(true); Dispose(true);
@ -1337,11 +1286,4 @@ namespace Emby.Server.Implementations
_disposed = true; _disposed = true;
} }
} }
internal class CertificateInfo
{
public string Path { get; set; }
public string Password { get; set; }
}
} }

View File

@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Archiving
options.Overwrite = true; options.Overwrite = true;
} }
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options); reader.WriteAllToDirectory(targetPath, options);
} }
@ -58,6 +59,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles Overwrite = overwriteExistingFiles
}; };
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options); reader.WriteAllToDirectory(targetPath, options);
} }
@ -71,6 +73,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles Overwrite = overwriteExistingFiles
}; };
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options); reader.WriteAllToDirectory(targetPath, options);
} }
@ -120,6 +123,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles Overwrite = overwriteExistingFiles
}; };
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options); reader.WriteAllToDirectory(targetPath, options);
} }
@ -151,6 +155,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles Overwrite = overwriteExistingFiles
}; };
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options); reader.WriteAllToDirectory(targetPath, options);
} }
} }

View File

@ -10,8 +10,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -815,7 +815,7 @@ namespace Emby.Server.Implementations.Channels
{ {
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
await using FileStream jsonStream = File.OpenRead(cachePath); await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null) if (cachedResult != null)
{ {
@ -838,7 +838,7 @@ namespace Emby.Server.Implementations.Channels
{ {
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
await using FileStream jsonStream = File.OpenRead(cachePath); await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null) if (cachedResult != null)
{ {

View File

@ -196,8 +196,8 @@ namespace Emby.Server.Implementations.Collections
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids) public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{ {

View File

@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// <value>The write connection.</value> /// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; } protected SQLiteDatabaseConnection WriteConnection { get; set; }
protected ManagedConnection GetConnection(bool _ = false) protected ManagedConnection GetConnection(bool readOnly = false)
{ {
WriteLock.Wait(); WriteLock.Wait();
if (WriteConnection != null) if (WriteConnection != null)
@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
_disposed = true; _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

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

View File

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

View File

@ -16,7 +16,6 @@ using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -25,7 +24,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
@ -48,6 +46,11 @@ namespace Emby.Server.Implementations.Data
private const string FromText = " from TypedBaseItems A"; private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2"; private const string ChaptersTableName = "Chapters2";
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
@ -57,6 +60,231 @@ namespace Emby.Server.Implementations.Data
private readonly TypeMapper _typeMapper; private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
private static readonly string[] _retriveItemColumns =
{
"type",
"data",
"StartDate",
"EndDate",
"ChannelId",
"IsMovie",
"IsSeries",
"EpisodeTitle",
"IsRepeat",
"CommunityRating",
"CustomRating",
"IndexNumber",
"IsLocked",
"PreferredMetadataLanguage",
"PreferredMetadataCountryCode",
"Width",
"Height",
"DateLastRefreshed",
"Name",
"Path",
"PremiereDate",
"Overview",
"ParentIndexNumber",
"ProductionYear",
"OfficialRating",
"ForcedSortName",
"RunTimeTicks",
"Size",
"DateCreated",
"DateModified",
"guid",
"Genres",
"ParentId",
"Audio",
"ExternalServiceId",
"IsInMixedFolder",
"DateLastSaved",
"LockedFields",
"Studios",
"Tags",
"TrailerTypes",
"OriginalTitle",
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
"CriticRating",
"IsVirtualItem",
"SeriesName",
"SeasonName",
"SeasonId",
"SeriesId",
"PresentationUniqueKey",
"InheritedParentalRatingValue",
"ExternalSeriesId",
"Tagline",
"ProviderIds",
"Images",
"ProductionLocations",
"ExtraIds",
"TotalBitrate",
"ExtraType",
"Artists",
"AlbumArtists",
"ExternalId",
"SeriesPresentationUniqueKey",
"ShowId",
"OwnerId"
};
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
"StreamIndex",
"StreamType",
"Codec",
"Language",
"ChannelLayout",
"Profile",
"AspectRatio",
"Path",
"IsInterlaced",
"BitRate",
"Channels",
"SampleRate",
"IsDefault",
"IsForced",
"IsExternal",
"Height",
"Width",
"AverageFrameRate",
"RealFrameRate",
"Level",
"PixelFormat",
"BitDepth",
"IsAnamorphic",
"RefFrames",
"CodecTag",
"Comment",
"NalLengthSize",
"IsAvc",
"Title",
"TimeBase",
"CodecTimeBase",
"ColorPrimaries",
"ColorSpace",
"ColorTransfer"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
private static readonly string _mediaStreamSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
"AttachmentIndex",
"Codec",
"CodecTag",
"Comment",
"Filename",
"MIMEType"
};
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
private static readonly string _mediaAttachmentInsertPrefix;
private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"TvChannel",
"LiveTvProgram",
"LiveTvTvChannel"
};
private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"MusicAlbum",
"MusicArtist",
"PhotoAlbum"
};
private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"TvChannel",
"LiveTvTvChannel"
};
private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"LiveTvProgram"
};
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Book",
"AudioBook",
"Episode",
"Season"
};
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"PhotoAlbum"
};
private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Audio",
"MusicAlbum",
"MusicVideo",
"AudioBook",
"AudioPodcast"
};
private static readonly Type[] _knownTypes =
{
typeof(LiveTvProgram),
typeof(LiveTvChannel),
typeof(Series),
typeof(Audio),
typeof(MusicAlbum),
typeof(MusicArtist),
typeof(MusicGenre),
typeof(MusicVideo),
typeof(Movie),
typeof(Playlist),
typeof(AudioBook),
typeof(Trailer),
typeof(BoxSet),
typeof(Episode),
typeof(Season),
typeof(Series),
typeof(Book),
typeof(CollectionFolder),
typeof(Folder),
typeof(Genre),
typeof(Person),
typeof(Photo),
typeof(PhotoAlbum),
typeof(Studio),
typeof(UserRootFolder),
typeof(UserView),
typeof(Video),
typeof(Year),
typeof(Channel),
typeof(AggregateFolder)
};
private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
static SqliteItemRepository() static SqliteItemRepository()
{ {
var queryPrefixText = new StringBuilder(); var queryPrefixText = new StringBuilder();
@ -117,6 +345,8 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Opens the connection to the database. /// Opens the connection to the database.
/// </summary> /// </summary>
/// <param name="userDataRepo">The user data repository.</param>
/// <param name="userManager">The user manager.</param>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{ {
const string CreateMediaStreamsTableCommand const string CreateMediaStreamsTableCommand
@ -156,7 +386,7 @@ namespace Emby.Server.Implementations.Data
"drop index if exists idx_TypedBaseItems", "drop index if exists idx_TypedBaseItems",
"drop index if exists idx_mediastreams", "drop index if exists idx_mediastreams",
"drop index if exists idx_mediastreams1", "drop index if exists idx_mediastreams1",
"drop index if exists idx_"+ChaptersTableName, "drop index if exists idx_" + ChaptersTableName,
"drop index if exists idx_UserDataKeys1", "drop index if exists idx_UserDataKeys1",
"drop index if exists idx_UserDataKeys2", "drop index if exists idx_UserDataKeys2",
"drop index if exists idx_TypeTopParentId3", "drop index if exists idx_TypeTopParentId3",
@ -342,151 +572,12 @@ namespace Emby.Server.Implementations.Data
userDataRepo.Initialize(userManager, WriteLock, WriteConnection); userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
} }
private static readonly string[] _retriveItemColumns =
{
"type",
"data",
"StartDate",
"EndDate",
"ChannelId",
"IsMovie",
"IsSeries",
"EpisodeTitle",
"IsRepeat",
"CommunityRating",
"CustomRating",
"IndexNumber",
"IsLocked",
"PreferredMetadataLanguage",
"PreferredMetadataCountryCode",
"Width",
"Height",
"DateLastRefreshed",
"Name",
"Path",
"PremiereDate",
"Overview",
"ParentIndexNumber",
"ProductionYear",
"OfficialRating",
"ForcedSortName",
"RunTimeTicks",
"Size",
"DateCreated",
"DateModified",
"guid",
"Genres",
"ParentId",
"Audio",
"ExternalServiceId",
"IsInMixedFolder",
"DateLastSaved",
"LockedFields",
"Studios",
"Tags",
"TrailerTypes",
"OriginalTitle",
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
"CriticRating",
"IsVirtualItem",
"SeriesName",
"SeasonName",
"SeasonId",
"SeriesId",
"PresentationUniqueKey",
"InheritedParentalRatingValue",
"ExternalSeriesId",
"Tagline",
"ProviderIds",
"Images",
"ProductionLocations",
"ExtraIds",
"TotalBitrate",
"ExtraType",
"Artists",
"AlbumArtists",
"ExternalId",
"SeriesPresentationUniqueKey",
"ShowId",
"OwnerId"
};
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
"StreamIndex",
"StreamType",
"Codec",
"Language",
"ChannelLayout",
"Profile",
"AspectRatio",
"Path",
"IsInterlaced",
"BitRate",
"Channels",
"SampleRate",
"IsDefault",
"IsForced",
"IsExternal",
"Height",
"Width",
"AverageFrameRate",
"RealFrameRate",
"Level",
"PixelFormat",
"BitDepth",
"IsAnamorphic",
"RefFrames",
"CodecTag",
"Comment",
"NalLengthSize",
"IsAvc",
"Title",
"TimeBase",
"CodecTimeBase",
"ColorPrimaries",
"ColorSpace",
"ColorTransfer"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
private static readonly string _mediaStreamSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
"AttachmentIndex",
"Codec",
"CodecTag",
"Comment",
"Filename",
"MIMEType"
};
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
private static readonly string _mediaAttachmentInsertPrefix;
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
/// <summary> /// <summary>
/// Save a standard item in the repo. /// Save a standard item in the repo.
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception> /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
public void SaveItem(BaseItem item, CancellationToken cancellationToken) public void SaveItem(BaseItem item, CancellationToken cancellationToken)
{ {
if (item == null) if (item == null)
@ -511,7 +602,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction( connection.RunInTransaction(
db => db =>
{ {
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{ {
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray()); saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
@ -528,9 +619,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="items">The items.</param> /// <param name="items">The items.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// items /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
/// or
/// cancellationToken
/// </exception> /// </exception>
public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken) public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
{ {
@ -1152,7 +1241,7 @@ namespace Emby.Server.Implementations.Data
return null; return null;
} }
if (Enum.TryParse(imageType.ToString(), true, out ImageType type)) if (Enum.TryParse(imageType, true, out ImageType type))
{ {
image.Type = type; image.Type = type;
} }
@ -1218,8 +1307,8 @@ namespace Emby.Server.Implementations.Data
/// </summary> /// </summary>
/// <param name="id">The id.</param> /// <param name="id">The id.</param>
/// <returns>BaseItem.</returns> /// <returns>BaseItem.</returns>
/// <exception cref="ArgumentNullException">id</exception> /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
public BaseItem RetrieveItem(Guid id) public BaseItem RetrieveItem(Guid id)
{ {
if (id == Guid.Empty) if (id == Guid.Empty)
@ -1573,7 +1662,6 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(index++, out var audioString)) if (reader.TryGetString(index++, out var audioString))
{ {
// TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
if (Enum.TryParse(audioString, true, out ProgramAudio audio)) if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{ {
item.Audio = audio; item.Audio = audio;
@ -1612,18 +1700,16 @@ namespace Emby.Server.Implementations.Data
{ {
if (reader.TryGetString(index++, out var lockedFields)) if (reader.TryGetString(index++, out var lockedFields))
{ {
IEnumerable<MetadataField> GetLockedFields(string s) List<MetadataField> fields = null;
foreach (var i in lockedFields.AsSpan().Split('|'))
{ {
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries)) if (Enum.TryParse(i, true, out MetadataField parsedValue))
{ {
if (Enum.TryParse(i, true, out MetadataField parsedValue)) (fields ??= new List<MetadataField>()).Add(parsedValue);
{
yield return parsedValue;
}
} }
} }
item.LockedFields = GetLockedFields(lockedFields).ToArray(); item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
} }
} }
@ -1649,18 +1735,16 @@ namespace Emby.Server.Implementations.Data
{ {
if (reader.TryGetString(index, out var trailerTypes)) if (reader.TryGetString(index, out var trailerTypes))
{ {
IEnumerable<TrailerType> GetTrailerTypes(string s) List<TrailerType> types = null;
foreach (var i in trailerTypes.AsSpan().Split('|'))
{ {
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries)) if (Enum.TryParse(i, true, out TrailerType parsedValue))
{ {
if (Enum.TryParse(i, true, out TrailerType parsedValue)) (types ??= new List<TrailerType>()).Add(parsedValue);
{
yield return parsedValue;
}
} }
} }
trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray(); trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
} }
} }
@ -1902,12 +1986,7 @@ namespace Emby.Server.Implementations.Data
return result; return result;
} }
/// <summary> /// <inheritdoc />
/// Gets chapters for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>IEnumerable{ChapterInfo}.</returns>
/// <exception cref="ArgumentNullException">id</exception>
public List<ChapterInfo> GetChapters(BaseItem item) public List<ChapterInfo> GetChapters(BaseItem item)
{ {
CheckDisposed(); CheckDisposed();
@ -1930,13 +2009,7 @@ namespace Emby.Server.Implementations.Data
} }
} }
/// <summary> /// <inheritdoc />
/// Gets a single chapter for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="index">The index.</param>
/// <returns>ChapterInfo.</returns>
/// <exception cref="ArgumentNullException">id</exception>
public ChapterInfo GetChapter(BaseItem item, int index) public ChapterInfo GetChapter(BaseItem item, int index)
{ {
CheckDisposed(); CheckDisposed();
@ -2004,6 +2077,8 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Saves the chapters. /// Saves the chapters.
/// </summary> /// </summary>
/// <param name="id">The item id.</param>
/// <param name="chapters">The chapters.</param>
public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters) public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
{ {
CheckDisposed(); CheckDisposed();
@ -2048,7 +2123,7 @@ namespace Emby.Server.Implementations.Data
for (var i = startIndex; i < endIndex; i++) for (var i = startIndex; i < endIndex; i++)
{ {
insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
} }
insertText.Length -= 1; // Remove last , insertText.Length -= 1; // Remove last ,
@ -2103,8 +2178,6 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue; || query.IsLiked.HasValue;
} }
private readonly ItemFields[] _allFields = Enum.GetValues<ItemFields>();
private bool HasField(InternalItemsQuery query, ItemFields name) private bool HasField(InternalItemsQuery query, ItemFields name)
{ {
switch (name) switch (name)
@ -2137,23 +2210,6 @@ namespace Emby.Server.Implementations.Data
} }
} }
private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"MusicAlbum",
"MusicArtist",
"PhotoAlbum"
};
private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"TvChannel",
"LiveTvProgram",
"LiveTvTvChannel"
};
private bool HasProgramAttributes(InternalItemsQuery query) private bool HasProgramAttributes(InternalItemsQuery query)
{ {
if (_programExcludeParentTypes.Contains(query.ParentType)) if (_programExcludeParentTypes.Contains(query.ParentType))
@ -2169,12 +2225,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _programTypes.Contains(x)); return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
} }
private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"TvChannel",
"LiveTvTvChannel"
};
private bool HasServiceName(InternalItemsQuery query) private bool HasServiceName(InternalItemsQuery query)
{ {
if (_programExcludeParentTypes.Contains(query.ParentType)) if (_programExcludeParentTypes.Contains(query.ParentType))
@ -2190,12 +2240,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x)); return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
} }
private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"LiveTvProgram"
};
private bool HasStartDate(InternalItemsQuery query) private bool HasStartDate(InternalItemsQuery query)
{ {
if (_programExcludeParentTypes.Contains(query.ParentType)) if (_programExcludeParentTypes.Contains(query.ParentType))
@ -2231,22 +2275,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase); return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
} }
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"PhotoAlbum"
};
private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Audio",
"MusicAlbum",
"MusicVideo",
"AudioBook",
"AudioPodcast"
};
private bool HasArtistFields(InternalItemsQuery query) private bool HasArtistFields(InternalItemsQuery query)
{ {
if (_artistExcludeParentTypes.Contains(query.ParentType)) if (_artistExcludeParentTypes.Contains(query.ParentType))
@ -2262,14 +2290,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x)); return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
} }
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Book",
"AudioBook",
"Episode",
"Season"
};
private bool HasSeriesFields(InternalItemsQuery query) private bool HasSeriesFields(InternalItemsQuery query)
{ {
if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase)) if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
@ -2287,7 +2307,7 @@ namespace Emby.Server.Implementations.Data
private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns) private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{ {
foreach (var field in _allFields) foreach (var field in _allItemFields)
{ {
if (!HasField(query, field)) if (!HasField(query, field))
{ {
@ -4829,40 +4849,6 @@ namespace Emby.Server.Implementations.Data
return false; return false;
} }
private static readonly Type[] _knownTypes =
{
typeof(LiveTvProgram),
typeof(LiveTvChannel),
typeof(Series),
typeof(Audio),
typeof(MusicAlbum),
typeof(MusicArtist),
typeof(MusicGenre),
typeof(MusicVideo),
typeof(Movie),
typeof(Playlist),
typeof(AudioBook),
typeof(Trailer),
typeof(BoxSet),
typeof(Episode),
typeof(Season),
typeof(Series),
typeof(Book),
typeof(CollectionFolder),
typeof(Folder),
typeof(Genre),
typeof(Person),
typeof(Photo),
typeof(PhotoAlbum),
typeof(Studio),
typeof(UserRootFolder),
typeof(UserView),
typeof(Video),
typeof(Year),
typeof(Channel),
typeof(AggregateFolder)
};
public void UpdateInheritedValues() public void UpdateInheritedValues()
{ {
string sql = string.Join( string sql = string.Join(
@ -4904,9 +4890,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return dict; return dict;
} }
// Not crazy about having this all the way down here, but at least it's in one place
private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
private string MapIncludeItemTypes(string value) private string MapIncludeItemTypes(string value)
{ {
if (_types.TryGetValue(value, out string result)) if (_types.TryGetValue(value, out string result))

View File

@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Opens the connection to the database. /// Opens the connection to the database.
/// </summary> /// </summary>
/// <param name="userManager">The user manager.</param>
/// <param name="dbLock">The lock to use for database IO.</param>
/// <param name="dbConnection">The connection to use for database IO.</param>
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection) public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
{ {
WriteLock.Dispose(); WriteLock.Dispose();
@ -49,8 +52,8 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction( connection.RunInTransaction(
db => db =>
{ {
db.ExecuteAll(string.Join(';', new[] { db.ExecuteAll(string.Join(';', new[]
{
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", "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_userdata",
@ -129,19 +132,17 @@ namespace Emby.Server.Implementations.Data
return list; return list;
} }
/// <summary> /// <inheritdoc />
/// Saves the user data. public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
/// </summary>
public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{ {
if (userData == null) if (userData == null)
{ {
throw new ArgumentNullException(nameof(userData)); throw new ArgumentNullException(nameof(userData));
} }
if (internalUserId <= 0) if (userId <= 0)
{ {
throw new ArgumentNullException(nameof(internalUserId)); throw new ArgumentNullException(nameof(userId));
} }
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))
@ -149,22 +150,23 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
PersistUserData(internalUserId, key, userData, cancellationToken); PersistUserData(userId, key, userData, cancellationToken);
} }
public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken) /// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{ {
if (userData == null) if (userData == null)
{ {
throw new ArgumentNullException(nameof(userData)); throw new ArgumentNullException(nameof(userData));
} }
if (internalUserId <= 0) if (userId <= 0)
{ {
throw new ArgumentNullException(nameof(internalUserId)); throw new ArgumentNullException(nameof(userId));
} }
PersistAllUserData(internalUserId, userData, cancellationToken); PersistAllUserData(userId, userData, cancellationToken);
} }
/// <summary> /// <summary>
@ -263,19 +265,19 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Gets the user data. /// Gets the user data.
/// </summary> /// </summary>
/// <param name="internalUserId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="key">The key.</param> /// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns> /// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// userId /// userId
/// or /// or
/// key /// key.
/// </exception> /// </exception>
public UserItemData GetUserData(long internalUserId, string key) public UserItemData GetUserData(long userId, string key)
{ {
if (internalUserId <= 0) if (userId <= 0)
{ {
throw new ArgumentNullException(nameof(internalUserId)); throw new ArgumentNullException(nameof(userId));
} }
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))
@ -287,7 +289,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")) 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", internalUserId); statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key); statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
@ -300,7 +302,7 @@ namespace Emby.Server.Implementations.Data
} }
} }
public UserItemData GetUserData(long internalUserId, List<string> keys) public UserItemData GetUserData(long userId, List<string> keys)
{ {
if (keys == null) if (keys == null)
{ {
@ -312,19 +314,19 @@ namespace Emby.Server.Implementations.Data
return null; return null;
} }
return GetUserData(internalUserId, keys[0]); return GetUserData(userId, keys[0]);
} }
/// <summary> /// <summary>
/// Return all user-data associated with the given user. /// Return all user-data associated with the given user.
/// </summary> /// </summary>
/// <param name="internalUserId">The internal user id.</param> /// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns> /// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long internalUserId) public List<UserItemData> GetAllUserData(long userId)
{ {
if (internalUserId <= 0) if (userId <= 0)
{ {
throw new ArgumentNullException(nameof(internalUserId)); throw new ArgumentNullException(nameof(userId));
} }
var list = new List<UserItemData>(); var list = new List<UserItemData>();
@ -333,7 +335,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")) using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{ {
statement.TryBind("@UserId", internalUserId); statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
{ {

View File

@ -0,0 +1,30 @@
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

@ -0,0 +1,23 @@
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,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger; private readonly ILogger<DeviceId> _logger;
private readonly object _syncLock = new object(); 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 CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
private string GetCachedId() private string GetCachedId()
@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
return id; 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

@ -1,146 +0,0 @@
#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

@ -51,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
public DtoService( public DtoService(
ILogger<DtoService> logger, ILogger<DtoService> logger,
ILibraryManager libraryManager, ILibraryManager libraryManager,
@ -75,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory; _livetvManagerFactory = livetvManagerFactory;
} }
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{ {
@ -507,7 +507,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary> /// </summary>
/// <param name="dto">The dto.</param> /// <param name="dto">The dto.</param>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachPeople(BaseItemDto dto, BaseItem item) private void AttachPeople(BaseItemDto dto, BaseItem item)
{ {
// Ordering by person type to ensure actors and artists are at the front. // Ordering by person type to ensure actors and artists are at the front.
@ -616,7 +615,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary> /// </summary>
/// <param name="dto">The dto.</param> /// <param name="dto">The dto.</param>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachStudios(BaseItemDto dto, BaseItem item) private void AttachStudios(BaseItemDto dto, BaseItem item)
{ {
dto.Studios = item.Studios dto.Studios = item.Studios
@ -1313,9 +1311,12 @@ namespace Emby.Server.Implementations.Dto
var imageTags = dto.ImageTags; 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)
(parent ??= (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|| parent is Series)
{ {
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
if (parent == null) if (parent == null)
{ {
break; break;
@ -1395,7 +1396,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary> /// </summary>
/// <param name="dto">The dto.</param> /// <param name="dto">The dto.</param>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <returns>Task.</returns>
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item) public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
{ {
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item); dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);

View File

@ -23,18 +23,18 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.4" /> <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.10" />
<PackageReference Include="Mono.Nat" Version="3.0.1" /> <PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.1" />
<PackageReference Include="sharpcompress" Version="0.28.3" /> <PackageReference Include="sharpcompress" Version="0.30.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -42,16 +42,11 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn> <NoWarn>AD0001</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View File

@ -9,7 +9,6 @@ using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;

View File

@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary> /// <summary>
/// Translates the physical item to user library. /// Translates the physical item to user library.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T">The type of item.</typeparam>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param> /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext; _authorizationContext = authorizationContext;
} }
public AuthorizationInfo Authenticate(HttpRequest request) public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{ {
var auth = _authorizationContext.GetAuthorizationInfo(request); var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken) if (!auth.HasToken)
{ {

View File

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

View File

@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary> /// <summary>
/// Sends a message asynchronously. /// Sends a message asynchronously.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message.</param> /// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{ {
await ProcessInternal(pipe.Reader).ConfigureAwait(false); 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); && receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty); Closed?.Invoke(this, EventArgs.Empty);

View File

@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc /> /// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context) public async Task WebSocketRequestHandler(HttpContext context)
{ {
_ = _authService.Authenticate(context.Request); _ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
try try
{ {
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View File

@ -41,6 +41,25 @@ namespace Emby.Server.Implementations.IO
private bool _disposed = false; 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> /// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary> /// </summary>
@ -95,21 +114,6 @@ 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) private bool IsLibraryMonitorEnabled(BaseItem item)
{ {
if (item is BasePluginFolder) if (item is BasePluginFolder)
@ -199,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="lst">The LST.</param> /// <param name="lst">The LST.</param>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">path</exception> /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path) private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))

View File

@ -5,11 +5,9 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
@ -19,7 +17,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
public class ManagedFileSystem : IFileSystem public class ManagedFileSystem : IFileSystem
{ {
protected ILogger<ManagedFileSystem> Logger; private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath; private readonly string _tempPath;
@ -29,7 +27,7 @@ namespace Emby.Server.Implementations.IO
ILogger<ManagedFileSystem> logger, ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths) IApplicationPaths applicationPaths)
{ {
Logger = logger; _logger = logger;
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
} }
@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="filename">The filename.</param> /// <param name="filename">The filename.</param>
/// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">filename</exception> /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual bool IsShortcut(string filename) public virtual bool IsShortcut(string filename)
{ {
if (string.IsNullOrEmpty(filename)) if (string.IsNullOrEmpty(filename))
@ -60,7 +58,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="filename">The filename.</param> /// <param name="filename">The filename.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">filename</exception> /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual string? ResolveShortcut(string filename) public virtual string? ResolveShortcut(string filename)
{ {
if (string.IsNullOrEmpty(filename)) if (string.IsNullOrEmpty(filename))
@ -235,9 +233,9 @@ namespace Emby.Server.Implementations.IO
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory; result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory) // if (!result.IsDirectory)
//{ // {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
//} // }
if (info is FileInfo fileInfo) if (info is FileInfo fileInfo)
{ {
@ -248,15 +246,15 @@ namespace Emby.Server.Implementations.IO
{ {
try try
{ {
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{ {
result.Length = thisFileStream.Length; result.Length = RandomAccess.GetLength(fileHandle);
} }
} }
catch (FileNotFoundException ex) catch (FileNotFoundException ex)
{ {
// Dangling symlinks cannot be detected before opening the file unfortunately... // 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; result.Exists = false;
} }
} }
@ -345,7 +343,7 @@ namespace Emby.Server.Implementations.IO
} }
catch (Exception ex) 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; return DateTime.MinValue;
} }
} }
@ -384,7 +382,7 @@ namespace Emby.Server.Implementations.IO
} }
catch (Exception ex) 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; return DateTime.MinValue;
} }
} }
@ -423,7 +421,7 @@ namespace Emby.Server.Implementations.IO
} }
} }
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
{ {
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{ {
@ -437,14 +435,14 @@ namespace Emby.Server.Implementations.IO
return; return;
} }
if (info.IsReadOnly == isReadOnly && info.IsHidden == isHidden) if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
{ {
return; return;
} }
var attributes = File.GetAttributes(path); var attributes = File.GetAttributes(path);
if (isReadOnly) if (readOnly)
{ {
attributes = attributes | FileAttributes.ReadOnly; attributes = attributes | FileAttributes.ReadOnly;
} }

View File

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

View File

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Images
public int Order => 0; public int Order => 0;
protected virtual bool Supports(BaseItem _) => true; protected virtual bool Supports(BaseItem item) => true;
public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
@ -65,13 +65,13 @@ namespace Emby.Server.Implementations.Images
if (SupportedImages.Contains(ImageType.Primary)) if (SupportedImages.Contains(ImageType.Primary))
{ {
var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false); var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | primaryResult; updateType |= primaryResult;
} }
if (SupportedImages.Contains(ImageType.Thumb)) if (SupportedImages.Contains(ImageType.Thumb))
{ {
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false); var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | thumbResult; updateType |= thumbResult;
} }
return updateType; return updateType;

View File

@ -0,0 +1,67 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
where T : Folder, new()
{
private readonly ILibraryManager _libraryManager;
public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Parent = item,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new (string, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
},
Limit = 1
});
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
}
protected override bool Supports(BaseItem item)
{
return item is T;
}
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
if (item is MusicAlbum)
{
return false;
}
return base.HasChangedByDate(item, image);
}
}
}

View File

@ -2,69 +2,16 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images namespace Emby.Server.Implementations.Images
{ {
public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
where T : Folder, new()
{
protected ILibraryManager _libraryManager;
public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Parent = item,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new System.ValueTuple<string, SortOrder>[]
{
new System.ValueTuple<string, SortOrder>(ItemSortBy.IsFolder, SortOrder.Ascending),
new System.ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
},
Limit = 1
});
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
}
protected override bool Supports(BaseItem item)
{
return item is T;
}
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
if (item is MusicAlbum)
{
return false;
}
return base.HasChangedByDate(item, image);
}
}
public class FolderImageProvider : BaseFolderImageProvider<Folder> public class FolderImageProvider : BaseFolderImageProvider<Folder>
{ {
public FolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) public FolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
@ -87,20 +34,4 @@ namespace Emby.Server.Implementations.Images
return true; return true;
} }
} }
public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
{
public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
{
public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
} }

View File

@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -19,46 +18,6 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images namespace Emby.Server.Implementations.Images
{ {
/// <summary>
/// Class MusicGenreImageProvider.
/// </summary>
public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
{
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get children objects used to create an music genre image.
/// </summary>
/// <param name="item">The music genre used to create the image.</param>
/// <returns>Any relevant children objects.</returns>
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
ImageTypes = new[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
});
}
}
/// <summary> /// <summary>
/// Class GenreImageProvider. /// Class GenreImageProvider.
/// </summary> /// </summary>

View File

@ -0,0 +1,19 @@
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Images
{
public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
{
public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
}

View File

@ -0,0 +1,59 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
/// <summary>
/// Class MusicGenreImageProvider.
/// </summary>
public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
{
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get children objects used to create an music genre image.
/// </summary>
/// <param name="item">The music genre used to create the image.</param>
/// <returns>Any relevant children objects.</returns>
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
ImageTypes = new[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
});
}
}
}

View File

@ -0,0 +1,19 @@
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Images
{
public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
{
public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
}

View File

@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Ignore trailer folders but allow it at the collection level // Ignore trailer folders but allow it at the collection level
if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
&& !(parent is AggregateFolder) && !(parent is AggregateFolder)
&& !(parent is UserRootFolder)) && !(parent is UserRootFolder))
{ {
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Don't resolve these into audio files // Don't resolve these into audio files
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal) if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename)) && _libraryManager.IsAudioFile(filename))
{ {
return true; return true;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -41,6 +42,11 @@ namespace Emby.Server.Implementations.Library
return _closeFn(); return _closeFn();
} }
public Stream GetStream()
{
throw new NotSupportedException();
}
public Task Open(CancellationToken openCancellationToken) public Task Open(CancellationToken openCancellationToken)
{ {
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -647,7 +647,7 @@ namespace Emby.Server.Implementations.Library
/// Determines whether a path should be ignored based on its contents - called after the contents have been read. /// Determines whether a path should be ignored based on its contents - called after the contents have been read.
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
private static bool ShouldResolvePathContents(ItemResolveArgs args) private static bool ShouldResolvePathContents(ItemResolveArgs args)
{ {
// Ignore any folders containing a file called .ignore // Ignore any folders containing a file called .ignore
@ -1250,10 +1250,8 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path) private CollectionTypeOptions? GetCollectionType(string path)
{ {
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false); var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
foreach (var file in files) foreach (ReadOnlySpan<char> file in files)
{ {
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res)) if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{ {
return res; return res;
@ -1268,7 +1266,7 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
/// <param name="id">The id.</param> /// <param name="id">The id.</param>
/// <returns>BaseItem.</returns> /// <returns>BaseItem.</returns>
/// <exception cref="ArgumentNullException">id</exception> /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
public BaseItem GetItemById(Guid id) public BaseItem GetItemById(Guid id)
{ {
if (id == Guid.Empty) if (id == Guid.Empty)
@ -1761,22 +1759,20 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items; return orderedItems ?? items;
} }
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList) public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy)
{ {
var isFirst = true; var isFirst = true;
IOrderedEnumerable<BaseItem> orderedItems = null; IOrderedEnumerable<BaseItem> orderedItems = null;
foreach (var orderBy in orderByList) foreach (var (name, sortOrder) in orderBy)
{ {
var comparer = GetComparer(orderBy.Item1, user); var comparer = GetComparer(name, user);
if (comparer == null) if (comparer == null)
{ {
continue; continue;
} }
var sortOrder = orderBy.Item2;
if (isFirst) if (isFirst)
{ {
orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer); orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
@ -2716,7 +2712,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions(); var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList(); .ToList();
@ -2760,7 +2756,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions(); var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)) .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList(); .ToList();
@ -3076,9 +3072,9 @@ namespace Emby.Server.Implementations.Library
}); });
} }
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
{ {
AddMediaPathInternal(virtualFolderName, pathInfo, true); AddMediaPathInternal(virtualFolderName, mediaPath, true);
} }
private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions) private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
@ -3131,11 +3127,11 @@ namespace Emby.Server.Implementations.Library
} }
} }
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo) public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
{ {
if (pathInfo == null) if (mediaPath == null)
{ {
throw new ArgumentNullException(nameof(pathInfo)); throw new ArgumentNullException(nameof(mediaPath));
} }
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
@ -3148,9 +3144,9 @@ namespace Emby.Server.Implementations.Library
var list = libraryOptions.PathInfos.ToList(); var list = libraryOptions.PathInfos.ToList();
foreach (var originalPathInfo in list) foreach (var originalPathInfo in list)
{ {
if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal)) if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
{ {
originalPathInfo.NetworkPath = pathInfo.NetworkPath; originalPathInfo.NetworkPath = mediaPath.NetworkPath;
break; break;
} }
} }

View File

@ -10,13 +10,14 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library
{ {
try try
{ {
await using FileStream jsonStream = File.OpenRead(cacheFilePath); await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info"); // _logger.LogDebug("Found cached media info");
@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null) if (cacheFilePath != null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = File.OpenWrite(cacheFilePath); await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath); // _logger.LogDebug("Saved media info to {0}", cacheFilePath);

View File

@ -13,9 +13,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@ -521,7 +521,7 @@ namespace Emby.Server.Implementations.Library
// TODO: @bond Fix // TODO: @bond Fix
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions); var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
_logger.LogInformation("Live stream opened: " + json); _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions); var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty)) if (!request.UserId.Equals(Guid.Empty))
@ -587,13 +587,6 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(); mediaSource.InferTotalBitrate();
} }
public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(info.Value as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{ {
var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false); var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
@ -602,7 +595,8 @@ namespace Emby.Server.Implementations.Library
public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken) public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
{ {
var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
var mediaSource = liveStreamInfo.MediaSource; var mediaSource = liveStreamInfo.MediaSource;
@ -638,7 +632,7 @@ namespace Emby.Server.Implementations.Library
{ {
try try
{ {
await using FileStream jsonStream = File.OpenRead(cacheFilePath); await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info"); // _logger.LogDebug("Found cached media info");
@ -771,18 +765,19 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(true); mediaSource.InferTotalBitrate(true);
} }
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(id))
{ {
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider); var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
} }
private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) public ILiveStream GetLiveStreamInfo(string id)
{ {
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(id))
{ {
@ -791,12 +786,16 @@ namespace Emby.Server.Implementations.Library
if (_openStreams.TryGetValue(id, out ILiveStream info)) if (_openStreams.TryGetValue(id, out ILiveStream info))
{ {
return Task.FromResult(info); return info;
}
else
{
return Task.FromException<ILiveStream>(new ResourceNotFoundException());
} }
return null;
}
/// <inheritdoc />
public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
{
return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase));
} }
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken) public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)

View File

@ -36,9 +36,10 @@ namespace Emby.Server.Implementations.Library
return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList(); return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList();
} }
public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions) /// <inheritdoc />
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions)
{ {
return GetInstantMixFromGenres(item.Genres, user, dtoOptions); return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
} }
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions)

View File

@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="path">The original path.</param> /// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param> /// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param> /// <param name="newSubPath">The new sub path.</param>
/// <param name="newPath">The result of the sub path replacement</param> /// <param name="newPath">The result of the sub path replacement.</param>
/// <returns>The path after replacing the sub path.</returns> /// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception> /// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
public static bool TryReplaceSubPath( public static bool TryReplaceSubPath(

View File

@ -82,6 +82,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary> /// <summary>
/// Determine if the supplied file data points to a music album. /// Determine if the supplied file data points to a music album.
/// </summary> /// </summary>
/// <param name="path">The path to check.</param>
/// <param name="directoryService">The directory service.</param>
/// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
public bool IsMusicAlbum(string path, IDirectoryService directoryService) public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{ {
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager); return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);

View File

@ -21,13 +21,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
where T : Video, new() where T : Video, new()
{ {
protected readonly ILibraryManager LibraryManager;
protected BaseVideoResolver(ILibraryManager libraryManager) protected BaseVideoResolver(ILibraryManager libraryManager)
{ {
LibraryManager = libraryManager; LibraryManager = libraryManager;
} }
protected ILibraryManager LibraryManager { get; }
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
@ -275,6 +275,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary> /// <summary>
/// Determines whether [is DVD directory] [the specified directory name]. /// Determines whether [is DVD directory] [the specified directory name].
/// </summary> /// </summary>
/// <param name="fullPath">The full path of the directory.</param>
/// <param name="directoryName">The name of the directory.</param>
/// <param name="directoryService">The directory service.</param>
/// <returns><c>true</c> if the provided directory is a DVD directory, <c>false</c> otherwise.</returns>
protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService) protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
{ {
if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase)) if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase))

View File

@ -49,13 +49,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
var bookFiles = args.FileSystemChildren.Where(f => var bookFiles = args.FileSystemChildren.Where(f =>
{ {
var fileExtension = Path.GetExtension(f.FullName) ?? var fileExtension = Path.GetExtension(f.FullName)
string.Empty; ?? string.Empty;
return _validExtensions.Contains( return _validExtensions.Contains(
fileExtension, fileExtension,
StringComparer StringComparer.OrdinalIgnoreCase);
.OrdinalIgnoreCase);
}).ToList(); }).ToList();
// Don't return a Book if there is more (or less) than one document in the directory // Don't return a Book if there is more (or less) than one document in the directory

View File

@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary> /// <summary>
/// Class FolderResolver. /// Class FolderResolver.
/// </summary> /// </summary>
public class FolderResolver : FolderResolver<Folder> public class FolderResolver : GenericFolderResolver<Folder>
{ {
/// <summary> /// <summary>
/// Gets the priority. /// Gets the priority.
@ -32,24 +32,4 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null; return null;
} }
} }
/// <summary>
/// Class FolderResolver.
/// </summary>
/// <typeparam name="TItemType">The type of the T item type.</typeparam>
public abstract class FolderResolver<TItemType> : ItemResolver<TItemType>
where TItemType : Folder, new()
{
/// <summary>
/// Sets the initial item values.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="args">The args.</param>
protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
{
base.SetInitialItemValues(item, args);
item.IsRoot = args.Parent == null;
}
}
} }

View File

@ -0,0 +1,27 @@
#nullable disable
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.Resolvers
{
/// <summary>
/// Class FolderResolver.
/// </summary>
/// <typeparam name="TItemType">The type of the T item type.</typeparam>
public abstract class GenericFolderResolver<TItemType> : ItemResolver<TItemType>
where TItemType : Folder, new()
{
/// <summary>
/// Sets the initial item values.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="args">The args.</param>
protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
{
base.SetInitialItemValues(item, args);
item.IsRoot = args.Parent == null;
}
}
}

View File

@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary> /// <summary>
/// Class BoxSetResolver. /// Class BoxSetResolver.
/// </summary> /// </summary>
public class BoxSetResolver : FolderResolver<BoxSet> public class BoxSetResolver : GenericFolderResolver<BoxSet>
{ {
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.

View File

@ -24,6 +24,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// </summary> /// </summary>
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{ {
private readonly IImageProcessor _imageProcessor;
private string[] _validCollectionTypes = new[] private string[] _validCollectionTypes = new[]
{ {
CollectionType.Movies, CollectionType.Movies,
@ -33,8 +35,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
CollectionType.Photos CollectionType.Photos
}; };
private readonly IImageProcessor _imageProcessor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class. /// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary> /// </summary>

View File

@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary> /// <summary>
/// Class PhotoAlbumResolver. /// Class PhotoAlbumResolver.
/// </summary> /// </summary>
public class PhotoAlbumResolver : FolderResolver<PhotoAlbum> public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;

View File

@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary> /// <summary>
/// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items. /// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
/// </summary> /// </summary>
public class PlaylistResolver : FolderResolver<Playlist> public class PlaylistResolver : GenericFolderResolver<Playlist>
{ {
private string[] _musicPlaylistCollectionTypes = private string[] _musicPlaylistCollectionTypes =
{ {

View File

@ -13,7 +13,7 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers namespace Emby.Server.Implementations.Library.Resolvers
{ {
public class SpecialFolderResolver : FolderResolver<Folder> public class SpecialFolderResolver : GenericFolderResolver<Folder>
{ {
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
@ -67,7 +67,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return args.FileSystemChildren return args.FileSystemChildren
.Where(i => .Where(i =>
{ {
try try
{ {
return !i.IsDirectory && return !i.IsDirectory &&

View File

@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if ((season != null || if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>()) args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase))) && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
{ {
var episode = ResolveVideo<Episode>(args, false); var episode = ResolveVideo<Episode>(args, false);

View File

@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary> /// <summary>
/// Class SeasonResolver. /// Class SeasonResolver.
/// </summary> /// </summary>
public class SeasonResolver : FolderResolver<Season> public class SeasonResolver : GenericFolderResolver<Season>
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;

View File

@ -8,7 +8,6 @@ using System.IO;
using Emby.Naming.TV; using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -19,7 +18,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary> /// <summary>
/// Class SeriesResolver. /// Class SeriesResolver.
/// </summary> /// </summary>
public class SeriesResolver : FolderResolver<Series> public class SeriesResolver : GenericFolderResolver<Series>
{ {
private readonly ILogger<SeriesResolver> _logger; private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;

View File

@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="query">The query.</param> /// <param name="query">The query.</param>
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
/// <returns>IEnumerable{SearchHintResult}.</returns> /// <returns>IEnumerable{SearchHintResult}.</returns>
/// <exception cref="ArgumentNullException">searchTerm</exception> /// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception>
private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user) private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
{ {
var searchTerm = query.SearchTerm; var searchTerm = query.SearchTerm;

View File

@ -25,8 +25,6 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class UserDataManager : IUserDataManager public class UserDataManager : IUserDataManager
{ {
public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
private readonly ConcurrentDictionary<string, UserItemData> _userData = private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
@ -44,6 +42,8 @@ namespace Emby.Server.Implementations.Library
_repository = repository; _repository = repository;
} }
public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@ -90,10 +90,9 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary> /// </summary>
/// <param name="userId"></param> /// <param name="userId">The user id.</param>
/// <param name="userData"></param> /// <param name="userData">The user item data.</param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@ -104,8 +103,8 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Retrieve all user data for the given user. /// Retrieve all user data for the given user.
/// </summary> /// </summary>
/// <param name="userId"></param> /// <param name="userId">The user id.</param>
/// <returns></returns> /// <returns>A <see cref="List{UserItemData}"/> containing all of the user's item data.</returns>
public List<UserItemData> GetAllUserData(Guid userId) public List<UserItemData> GetAllUserData(Guid userId)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@ -177,6 +176,7 @@ namespace Emby.Server.Implementations.Library
return dto; return dto;
} }
/// <inheritdoc />
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options) public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options)
{ {
var userData = GetUserData(user, item); var userData = GetUserData(user, item);
@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
/// <param name="data">The data.</param> /// <param name="data">The data.</param>
/// <returns>DtoUserItemData.</returns> /// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
private UserItemDataDto GetUserItemDataDto(UserItemData data) private UserItemDataDto GetUserItemDataDto(UserItemData data)
{ {
if (data == null) if (data == null)
@ -212,6 +212,7 @@ namespace Emby.Server.Implementations.Library
}; };
} }
/// <inheritdoc />
public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks) public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
{ {
var playedToCompletion = false; var playedToCompletion = false;

View File

@ -341,14 +341,16 @@ namespace Emby.Server.Implementations.Library
mediaTypes = mediaTypes.Distinct().ToList(); mediaTypes = mediaTypes.Distinct().ToList();
} }
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
{ ? new[]
nameof(Person), {
nameof(Studio), nameof(Person),
nameof(Year), nameof(Studio),
nameof(MusicGenre), nameof(Year),
nameof(Genre) nameof(MusicGenre),
} : Array.Empty<string>(); nameof(Genre)
}
: Array.Empty<string>();
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {

View File

@ -95,10 +95,13 @@ namespace Emby.Server.Implementations.Library.Validators
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
_libraryManager.DeleteItem(item, new DeleteOptions _libraryManager.DeleteItem(
{ item,
DeleteFileLocation = false new DeleteOptions
}, false); {
DeleteFileLocation = false
},
false);
} }
progress.Report(100); progress.Report(100);

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -46,20 +47,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None)) using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{ {
onStarted(); onStarted();
_logger.LogInformation("Copying recording stream to file {0}", targetFile); _logger.LogInformation("Copying recording to file {FilePath}", targetFile);
// The media source is infinite so we need to handle stopping ourselves // The media source is infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration); using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
var linkedCancellationToken = cancellationTokenSource.Token;
await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false); await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
await _streamHelper.CopyToAsync(
fileStream,
output,
IODefaults.CopyToBufferSize,
1000,
linkedCancellationToken).ConfigureAwait(false);
} }
_logger.LogInformation("Recording completed to file {0}", targetFile); _logger.LogInformation("Recording completed: {FilePath}", targetFile);
} }
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
@ -72,7 +80,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None); await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
onStarted(); onStarted();

View File

@ -610,11 +610,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken) public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
{ {
var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ? var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
null : null :
_timerProvider.GetTimerByProgramId(timer.ProgramId); _timerProvider.GetTimerByProgramId(info.ProgramId);
if (existingTimer != null) if (existingTimer != null)
{ {
@ -632,32 +632,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
} }
timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
LiveTvProgram programInfo = null; LiveTvProgram programInfo = null;
if (!string.IsNullOrWhiteSpace(timer.ProgramId)) if (!string.IsNullOrWhiteSpace(info.ProgramId))
{ {
programInfo = GetProgramInfoFromCache(timer); programInfo = GetProgramInfoFromCache(info);
} }
if (programInfo == null) if (programInfo == null)
{ {
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
} }
if (programInfo != null) if (programInfo != null)
{ {
CopyProgramInfoToTimerInfo(programInfo, timer); CopyProgramInfoToTimerInfo(programInfo, info);
} }
timer.IsManual = true; info.IsManual = true;
_timerProvider.Add(timer); _timerProvider.Add(info);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
return Task.FromResult(timer.Id); return Task.FromResult(info.Id);
} }
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
@ -1990,7 +1990,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString( writer.WriteElementString(
"dateadded", "dateadded",
DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture)); DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue) if (item.ProductionYear.HasValue)
{ {

View File

@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
@ -188,7 +188,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"", "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile, inputTempFile,
targetFile, targetFile.Replace("\"", "\\\""), // Escape quotes in filename
videoArgs, videoArgs,
GetAudioArgs(mediaSource), GetAudioArgs(mediaSource),
subtitleArgs, subtitleArgs,
@ -205,9 +205,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// var audioChannels = 2; // var audioChannels = 2;
// var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
// if (audioStream != null) // if (audioStream != null)
//{ // {
// audioChannels = audioStream.Channels ?? audioChannels; // audioChannels = audioStream.Channels ?? audioChannels;
//} // }
// return "-codec:a:0 aac -strict experimental -ab 320000"; // return "-codec:a:0 aac -strict experimental -ab 320000";
} }

View File

@ -13,6 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary> /// <summary>
/// Records the specified media source. /// Records the specified media source.
/// </summary> /// </summary>
/// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param>
/// <param name="mediaSource">The media source.</param>
/// <param name="targetFile">The target file.</param>
/// <param name="duration">The duration to record.</param>
/// <param name="onStarted">An action to perform when recording starts.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> that represents the recording operation.</returns>
Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken); Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile); string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);

View File

@ -1,9 +1,8 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
@ -18,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly string _dataPath; private readonly string _dataPath;
private readonly object _fileDataLock = new object(); private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[] _items; private T[]? _items;
public ItemDataProvider( public ItemDataProvider(
ILogger logger, ILogger logger,
@ -34,6 +33,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
protected Func<T, T, bool> EqualityComparer { get; } protected Func<T, T, bool> EqualityComparer { get; }
[MemberNotNull(nameof(_items))]
private void EnsureLoaded() private void EnsureLoaded()
{ {
if (_items != null) if (_items != null)
@ -49,6 +49,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var bytes = File.ReadAllBytes(_dataPath); var bytes = File.ReadAllBytes(_dataPath);
_items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions); _items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
if (_items == null)
{
Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
_items = Array.Empty<T>();
}
return; return;
} }
catch (JsonException ex) catch (JsonException ex)
@ -62,7 +68,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList() private void SaveList()
{ {
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath)); Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions); var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
File.WriteAllText(_dataPath, jsonString); File.WriteAllText(_dataPath, jsonString);
} }

View File

@ -10,6 +10,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.XmlTv; using Jellyfin.XmlTv;
using Jellyfin.XmlTv.Entities; using Jellyfin.XmlTv.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
{ {
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
} }
@ -89,11 +90,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return UnzipIfNeeded(path, cacheFile); return UnzipIfNeeded(path, cacheFile);
} }
private string UnzipIfNeeded(string originalUrl, string file) private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
{ {
string ext = Path.GetExtension(originalUrl.Split('?')[0]); ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
{ {
try try
{ {

View File

@ -65,6 +65,8 @@ namespace Emby.Server.Implementations.LiveTv
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
private bool _disposed = false;
public LiveTvManager( public LiveTvManager(
IServerConfigurationManager config, IServerConfigurationManager config,
ILogger<LiveTvManager> logger, ILogger<LiveTvManager> logger,
@ -520,7 +522,7 @@ namespace Emby.Server.Implementations.LiveTv
return item; return item;
} }
private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel)
{ {
var id = _tvDtoService.GetInternalProgramId(info.Id); var id = _tvDtoService.GetInternalProgramId(info.Id);
@ -559,8 +561,6 @@ namespace Emby.Server.Implementations.LiveTv
item.ParentId = channel.Id; item.ParentId = channel.Id;
// item.ChannelType = channelType;
item.Audio = info.Audio; item.Audio = info.Audio;
item.ChannelId = channel.Id; item.ChannelId = channel.Id;
item.CommunityRating ??= info.CommunityRating; item.CommunityRating ??= info.CommunityRating;
@ -772,7 +772,7 @@ namespace Emby.Server.Implementations.LiveTv
item.OnMetadataChanged(); item.OnMetadataChanged();
} }
return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated); return (item, isNew, isUpdated);
} }
public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null) public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
@ -1187,14 +1187,14 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var program in channelPrograms) foreach (var program in channelPrograms)
{ {
var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken); var programTuple = GetProgram(program, existingPrograms, currentChannel);
var programItem = programTuple.Item1; var programItem = programTuple.item;
if (programTuple.Item2) if (programTuple.isNew)
{ {
newPrograms.Add(programItem); newPrograms.Add(programItem);
} }
else if (programTuple.Item3) else if (programTuple.isUpdated)
{ {
updatedPrograms.Add(programItem); updatedPrograms.Add(programItem);
} }
@ -1385,10 +1385,10 @@ namespace Emby.Server.Implementations.LiveTv
// var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray(); // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
// return new QueryResult<BaseItem> // return new QueryResult<BaseItem>
//{ // {
// Items = items, // Items = items,
// TotalRecordCount = items.Length // TotalRecordCount = items.Length
//}; // };
dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray(); dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray();
} }
@ -1425,16 +1425,15 @@ namespace Emby.Server.Implementations.LiveTv
return result; return result;
} }
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null) public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null)
{ {
var programTuples = new List<Tuple<BaseItemDto, string, string>>(); var programTuples = new List<Tuple<BaseItemDto, string, string>>();
var hasChannelImage = fields.Contains(ItemFields.ChannelImage); var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo); var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo);
foreach (var tuple in tuples) foreach (var (item, dto) in programs)
{ {
var program = (LiveTvProgram)tuple.Item1; var program = (LiveTvProgram)item;
var dto = tuple.Item2;
dto.StartDate = program.StartDate; dto.StartDate = program.StartDate;
dto.EpisodeTitle = program.EpisodeTitle; dto.EpisodeTitle = program.EpisodeTitle;
@ -1871,11 +1870,11 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetItemById(internalChannelId); return _libraryManager.GetItemById(internalChannelId);
} }
public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> tuples, DtoOptions options, User user) public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray(); var channelIds = items.Select(i => i.Item2.Id).Distinct().ToArray();
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
@ -1896,7 +1895,7 @@ namespace Emby.Server.Implementations.LiveTv
var addCurrentProgram = options.AddCurrentProgram; var addCurrentProgram = options.AddCurrentProgram;
foreach (var tuple in tuples) foreach (var tuple in items)
{ {
var dto = tuple.Item1; var dto = tuple.Item1;
var channel = tuple.Item2; var channel = tuple.Item2;
@ -2118,17 +2117,13 @@ namespace Emby.Server.Implementations.LiveTv
}; };
} }
/// <summary> /// <inheritdoc />
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private bool _disposed = false;
/// <summary> /// <summary>
/// Releases unmanaged and - optionally - managed resources. /// Releases unmanaged and - optionally - managed resources.
/// </summary> /// </summary>
@ -2324,20 +2319,20 @@ namespace Emby.Server.Implementations.LiveTv
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
} }
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId) public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{ {
var config = GetConfiguration(); var config = GetConfiguration();
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray(); listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{ {
var list = listingsProviderInfo.ChannelMappings.ToList(); var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair list.Add(new NameValuePair
{ {
Name = tunerChannelId, Name = tunerChannelNumber,
Value = providerChannelId Value = providerChannelNumber
}); });
listingsProviderInfo.ChannelMappings = list.ToArray(); listingsProviderInfo.ChannelMappings = list.ToArray();
} }
@ -2357,10 +2352,10 @@ namespace Emby.Server.Implementations.LiveTv
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase)); return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
} }
public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> epgChannels) public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
{ {
var result = new TunerChannelMapping var result = new TunerChannelMapping
{ {
@ -2373,7 +2368,7 @@ namespace Emby.Server.Implementations.LiveTv
result.Name = tunerChannel.Number + " " + result.Name; result.Name = tunerChannel.Number + " " + result.Name;
} }
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels); var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
if (providerChannel != null) if (providerChannel != null)
{ {

View File

@ -23,10 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
public abstract class BaseTunerHost public abstract class BaseTunerHost
{ {
protected readonly IServerConfigurationManager Config;
protected readonly ILogger<BaseTunerHost> Logger;
protected readonly IFileSystem FileSystem;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache) protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
@ -37,12 +33,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileSystem = fileSystem; FileSystem = fileSystem;
} }
protected IServerConfigurationManager Config { get; }
protected ILogger<BaseTunerHost> Logger { get; }
protected IFileSystem FileSystem { get; }
public virtual bool IsSupported => true; public virtual bool IsSupported => true;
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
public abstract string Type { get; } public abstract string Type { get; }
protected virtual string ChannelIdPrefix => Type + "_";
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken) public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{ {
var key = tuner.Id; var key = tuner.Id;
@ -92,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try try
{ {
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
await using var writeStream = File.OpenWrite(channelCacheFile); await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
} }
catch (IOException) catch (IOException)
@ -108,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
try try
{ {
await using var readStream = File.OpenRead(channelCacheFile); await using var readStream = AsyncFile.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken) var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
list.AddRange(channels); list.AddRange(channels);
@ -158,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return new List<MediaSourceInfo>(); return new List<MediaSourceInfo>();
} }
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{ {
@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException(); throw new LiveTvConflictException();
} }
protected virtual string ChannelIdPrefix => Type + "_";
protected virtual bool IsValidChannelId(string channelId) protected virtual bool IsValidChannelId(string channelId)
{ {
if (string.IsNullOrEmpty(channelId)) if (string.IsNullOrEmpty(channelId))

View File

@ -0,0 +1,35 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string? _channel;
private string? _profile;
public HdHomerunChannelCommands(string? channel, string? profile)
{
_channel = channel;
_profile = profile;
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
if (!string.IsNullOrEmpty(_profile)
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
{
yield return ("vchannel", $"{_channel} transcode={_profile}");
}
else
{
yield return ("vchannel", _channel);
}
}
}
}
}

View File

@ -36,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper; private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
@ -50,7 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ISocketFactory socketFactory, ISocketFactory socketFactory,
INetworkManager networkManager,
IStreamHelper streamHelper, IStreamHelper streamHelper,
IMemoryCache memoryCache) IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache) : base(config, logger, fileSystem, memoryCache)
@ -58,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
_socketFactory = socketFactory; _socketFactory = socketFactory;
_networkManager = networkManager;
_streamHelper = streamHelper; _streamHelper = streamHelper;
_jsonOptions = JsonDefaults.Options; _jsonOptions = JsonDefaults.Options;
@ -70,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override string ChannelIdPrefix => "hdhr_"; protected override string ChannelIdPrefix => "hdhr_";
private string GetChannelId(TunerHostInfo info, Channels i) private string GetChannelId(Channels i)
=> ChannelIdPrefix + i.GuideNumber; => ChannelIdPrefix + i.GuideNumber;
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
@ -90,22 +87,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return lineup.Where(i => !i.DRM).ToList(); return lineup.Where(i => !i.DRM).ToList();
} }
private class HdHomerunChannelInfo : ChannelInfo protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{ {
public bool IsLegacyTuner { get; set; } var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
}
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
{
var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
return lineup.Select(i => new HdHomerunChannelInfo return lineup.Select(i => new HdHomerunChannelInfo
{ {
Name = i.GuideName, Name = i.GuideName,
Number = i.GuideNumber, Number = i.GuideNumber,
Id = GetChannelId(info, i), Id = GetChannelId(i),
IsFavorite = i.Favorite, IsFavorite = i.Favorite,
TunerHostId = info.Id, TunerHostId = tuner.Id,
IsHD = i.HD, IsHD = i.HD,
AudioCodec = i.AudioCodec, AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec, VideoCodec = i.VideoCodec,
@ -255,7 +247,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
var tuners = new List<LiveTvTunerInfo>(); var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
var uri = new Uri(GetApiUrl(info)); var uri = new Uri(GetApiUrl(info));
@ -264,10 +256,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Legacy HdHomeruns are IPv4 only // Legacy HdHomeruns are IPv4 only
var ipInfo = IPAddress.Parse(uri.Host); var ipInfo = IPAddress.Parse(uri.Host);
for (int i = 0; i < model.TunerCount; ++i) for (int i = 0; i < model.TunerCount; i++)
{ {
var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1); var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
var currentChannel = "none"; // @todo Get current channel and map back to Station Id var currentChannel = "none"; // TODO: Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
tuners.Add(new LiveTvTunerInfo tuners.Add(new LiveTvTunerInfo
@ -455,28 +447,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Path = url, Path = url,
Protocol = MediaProtocol.Udp, Protocol = MediaProtocol.Udp,
MediaStreams = new List<MediaStream> MediaStreams = new List<MediaStream>
{ {
new MediaStream new MediaStream
{ {
Type = MediaStreamType.Video, Type = MediaStreamType.Video,
// Set the index to -1 because we don't know the exact index of the video stream within the container // Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1, Index = -1,
IsInterlaced = isInterlaced, IsInterlaced = isInterlaced,
Codec = videoCodec, Codec = videoCodec,
Width = width, Width = width,
Height = height, Height = height,
BitRate = videoBitrate, BitRate = videoBitrate,
NalLengthSize = nal NalLengthSize = nal
}, },
new MediaStream new MediaStream
{ {
Type = MediaStreamType.Audio, Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container // Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1, Index = -1,
Codec = audioCodec, Codec = audioCodec,
BitRate = audioBitrate BitRate = audioBitrate
} }
}, },
RequiresOpening = true, RequiresOpening = true,
RequiresClosing = true, RequiresClosing = true,
BufferMs = 0, BufferMs = 0,
@ -496,57 +488,53 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return mediaSource; return mediaSource;
} }
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken) protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{ {
var list = new List<MediaSourceInfo>(); var list = new List<MediaSourceInfo>();
var channelId = channelInfo.Id; var channelId = channel.Id;
var hdhrId = GetHdHrIdFromChannelId(channelId); var hdhrId = GetHdHrIdFromChannelId(channelId);
var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo; if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner)
var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
if (isLegacyTuner)
{ {
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
} }
else else
{ {
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false);
if (modelInfo != null && modelInfo.SupportsTranscoding) if (modelInfo != null && modelInfo.SupportsTranscoding)
{ {
if (info.AllowHWTranscoding) if (tuner.AllowHWTranscoding)
{ {
list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile"));
} }
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
} }
if (list.Count == 0) if (list.Count == 0)
{ {
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
} }
} }
return list; return list;
} }
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{ {
var tunerCount = info.TunerCount; var tunerCount = tunerHost.TunerCount;
if (tunerCount > 0) if (tunerCount > 0)
{ {
var tunerHostId = info.Id; var tunerHostId = tunerHost.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count() >= tunerCount) if (liveStreams.Count() >= tunerCount)
@ -555,28 +543,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
var profile = streamId.Split('_')[0]; var profile = streamId.AsSpan().LeftPart('_').ToString();
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile); Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id); var hdhrId = GetHdHrIdFromChannelId(channel.Id);
var hdhomerunChannel = channelInfo as HdHomerunChannelInfo; var hdhomerunChannel = channel as HdHomerunChannelInfo;
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false);
if (!modelInfo.SupportsTranscoding) if (!modelInfo.SupportsTranscoding)
{ {
profile = "native"; profile = "native";
} }
var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile);
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
{ {
return new HdHomerunUdpStream( return new HdHomerunUdpStream(
mediaSource, mediaSource,
info, tunerHost,
streamId, streamId,
new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
modelInfo.TunerCount, modelInfo.TunerCount,
@ -592,7 +580,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
mediaSource.Protocol = MediaProtocol.Http; mediaSource.Protocol = MediaProtocol.Http;
var httpUrl = channelInfo.Path; var httpUrl = channel.Path;
// If raw was used, the tuner doesn't support params // If raw was used, the tuner doesn't support params
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
@ -604,7 +592,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new SharedHttpStream( return new SharedHttpStream(
mediaSource, mediaSource,
info, tunerHost,
streamId, streamId,
FileSystem, FileSystem,
_httpClientFactory, _httpClientFactory,
@ -616,7 +604,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new HdHomerunUdpStream( return new HdHomerunUdpStream(
mediaSource, mediaSource,
info, tunerHost,
streamId, streamId,
new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
modelInfo.TunerCount, modelInfo.TunerCount,
@ -722,5 +710,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return hostInfo; return hostInfo;
} }
private class HdHomerunChannelInfo : ChannelInfo
{
public bool IsLegacyTuner { get; set; }
}
} }
} }

View File

@ -5,12 +5,10 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common; using MediaBrowser.Common;
@ -18,70 +16,6 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
public interface IHdHomerunChannelCommands
{
IEnumerable<(string, string)> GetCommands();
}
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string _channel;
private string _program;
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success)
{
_channel = match.Groups[1].Value;
_program = match.Groups[2].Value;
}
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
yield return ("channel", _channel);
}
if (!string.IsNullOrEmpty(_program))
{
yield return ("program", _program);
}
}
}
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string _channel;
private string _profile;
public HdHomerunChannelCommands(string channel, string profile)
{
_channel = channel;
_profile = profile;
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
if (!string.IsNullOrEmpty(_profile)
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
{
yield return ("vchannel", $"{_channel} transcode={_profile}");
}
else
{
yield return ("vchannel", _channel);
}
}
}
}
public sealed class HdHomerunManager : IDisposable public sealed class HdHomerunManager : IDisposable
{ {
public const int HdHomeRunPort = 65001; public const int HdHomeRunPort = 65001;

View File

@ -101,7 +101,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
if (localAddress.IsIPv4MappedToIPv6) { if (localAddress.IsIPv4MappedToIPv6)
{
localAddress = localAddress.MapToIPv4(); localAddress = localAddress.MapToIPv4();
} }
@ -156,11 +157,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false); await taskCompletionSource.Task.ConfigureAwait(false);
} }
public string GetFilePath()
{
return TempFilePath;
}
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{ {
using (udpClient) using (udpClient)
@ -184,7 +180,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = false; EnableStreamSharing = false;
} }
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
} }
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@ -201,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
cancellationToken, cancellationToken,
timeOutSource.Token)) timeOutSource.Token))
{ {
var resTask = udpClient.ReceiveAsync(); var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask) if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
{ {
resTask.Dispose(); resTask.Dispose();

View File

@ -0,0 +1,11 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public interface IHdHomerunChannelCommands
{
IEnumerable<(string, string)> GetCommands();
}
}

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