mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Merge branch 'master' into theorydata
This commit is contained in:
commit
8858d8e597
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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')) }}:
|
||||||
|
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@ -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:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
#####################################
|
#####################################
|
||||||
# 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
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
#####################################
|
#####################################
|
||||||
# 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
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
#####################################
|
#####################################
|
||||||
# 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
|
||||||
|
@ -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>
|
||||||
|
@ -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));
|
||||||
|
@ -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, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,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>
|
||||||
|
@ -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;
|
||||||
@ -82,9 +83,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, _usCulture, out var val))
|
||||||
|
|
||||||
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
|
||||||
{
|
{
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
@ -45,10 +45,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +88,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
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 +97,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
|
||||||
|
@ -250,8 +250,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()
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
|
@ -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, AsyncFile.UseAsyncIO))
|
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);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1150,7 +1150,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;
|
||||||
}
|
}
|
||||||
@ -1571,7 +1571,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;
|
||||||
@ -1610,18 +1609,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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1647,18 +1644,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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,16 +23,16 @@
|
|||||||
</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.1" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.1" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.28.3" />
|
<PackageReference Include="sharpcompress" Version="0.29.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.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@ -42,7 +42,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>
|
||||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||||
|
@ -246,9 +246,9 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (Stream thisFileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
|
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)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
@ -2714,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();
|
||||||
|
|
||||||
@ -2758,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();
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ 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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -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)
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
|
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, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO);
|
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
|
||||||
|
|
||||||
onStarted();
|
onStarted();
|
||||||
|
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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, AsyncFile.UseAsyncIO);
|
_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,
|
||||||
|
@ -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, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO))
|
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
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
@ -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))
|
||||||
|
@ -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)
|
||||||
@ -103,7 +100,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
{
|
{
|
||||||
Name = i.GuideName,
|
Name = i.GuideName,
|
||||||
Number = i.GuideNumber,
|
Number = i.GuideNumber,
|
||||||
Id = GetChannelId(tuner, i),
|
Id = GetChannelId(i),
|
||||||
IsFavorite = i.Favorite,
|
IsFavorite = i.Favorite,
|
||||||
TunerHostId = tuner.Id,
|
TunerHostId = tuner.Id,
|
||||||
IsHD = i.HD,
|
IsHD = i.HD,
|
||||||
@ -255,7 +252,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 +261,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 +452,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,
|
||||||
@ -551,7 +548,7 @@ 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}", channel.Id, streamId, profile);
|
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -3,10 +3,8 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
@ -22,14 +20,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
{
|
{
|
||||||
private readonly IConfigurationManager _configurationManager;
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
|
||||||
protected readonly IFileSystem FileSystem;
|
|
||||||
|
|
||||||
protected readonly IStreamHelper StreamHelper;
|
|
||||||
|
|
||||||
protected string TempFilePath;
|
|
||||||
protected readonly ILogger Logger;
|
|
||||||
protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
public LiveStream(
|
public LiveStream(
|
||||||
MediaSourceInfo mediaSource,
|
MediaSourceInfo mediaSource,
|
||||||
TunerHostInfo tuner,
|
TunerHostInfo tuner,
|
||||||
@ -57,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
SetTempFilePath("ts");
|
SetTempFilePath("ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual int EmptyReadLimit => 1000;
|
protected IFileSystem FileSystem { get; }
|
||||||
|
|
||||||
|
protected IStreamHelper StreamHelper { get; }
|
||||||
|
|
||||||
|
protected ILogger Logger { get; }
|
||||||
|
|
||||||
|
protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource();
|
||||||
|
|
||||||
|
protected string TempFilePath { get; set; }
|
||||||
|
|
||||||
public MediaSourceInfo OriginalMediaSource { get; set; }
|
public MediaSourceInfo OriginalMediaSource { get; set; }
|
||||||
|
|
||||||
@ -97,121 +95,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected FileStream GetInputStream(string path, bool allowAsyncFileRead)
|
public Stream GetStream()
|
||||||
|
{
|
||||||
|
var stream = GetInputStream(TempFilePath);
|
||||||
|
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
|
||||||
|
if (seekFile)
|
||||||
|
{
|
||||||
|
TrySeek(stream, -20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected FileStream GetInputStream(string path)
|
||||||
=> new FileStream(
|
=> new FileStream(
|
||||||
path,
|
path,
|
||||||
FileMode.Open,
|
FileMode.Open,
|
||||||
FileAccess.Read,
|
FileAccess.Read,
|
||||||
FileShare.ReadWrite,
|
FileShare.ReadWrite,
|
||||||
IODefaults.FileStreamBufferSize,
|
IODefaults.FileStreamBufferSize,
|
||||||
allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
|
FileOptions.SequentialScan | FileOptions.Asynchronous);
|
||||||
|
|
||||||
public Task DeleteTempFiles()
|
protected async Task DeleteTempFiles(string path, int retryCount = 0)
|
||||||
{
|
|
||||||
return DeleteTempFiles(GetStreamFilePaths());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task DeleteTempFiles(IEnumerable<string> paths, int retryCount = 0)
|
|
||||||
{
|
{
|
||||||
if (retryCount == 0)
|
if (retryCount == 0)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Deleting temp files {0}", paths);
|
Logger.LogInformation("Deleting temp file {FilePath}", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
var failedFiles = new List<string>();
|
try
|
||||||
|
|
||||||
foreach (var path in paths)
|
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
FileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error deleting file {FilePath}", path);
|
||||||
|
if (retryCount <= 40)
|
||||||
{
|
{
|
||||||
continue;
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
}
|
await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
FileSystem.DeleteFile(path);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error deleting file {path}", path);
|
|
||||||
failedFiles.Add(path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedFiles.Count > 0 && retryCount <= 40)
|
|
||||||
{
|
|
||||||
await Task.Delay(500).ConfigureAwait(false);
|
|
||||||
await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual List<string> GetStreamFilePaths()
|
private void TrySeek(Stream stream, long offset)
|
||||||
{
|
|
||||||
return new List<string> { TempFilePath };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
|
|
||||||
cancellationToken = linkedCancellationTokenSource.Token;
|
|
||||||
|
|
||||||
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
|
|
||||||
|
|
||||||
var nextFileInfo = GetNextFile(null);
|
|
||||||
var nextFile = nextFileInfo.file;
|
|
||||||
var isLastFile = nextFileInfo.isLastFile;
|
|
||||||
|
|
||||||
var allowAsync = AsyncFile.UseAsyncIO;
|
|
||||||
while (!string.IsNullOrEmpty(nextFile))
|
|
||||||
{
|
|
||||||
var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
|
|
||||||
|
|
||||||
await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
seekFile = false;
|
|
||||||
nextFileInfo = GetNextFile(nextFile);
|
|
||||||
nextFile = nextFileInfo.file;
|
|
||||||
isLastFile = nextFileInfo.isLastFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogInformation("Live Stream ended.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private (string file, bool isLastFile) GetNextFile(string currentFile)
|
|
||||||
{
|
|
||||||
var files = GetStreamFilePaths();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(currentFile))
|
|
||||||
{
|
|
||||||
return (files[^1], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
|
|
||||||
|
|
||||||
var isLastFile = nextIndex == files.Count - 1;
|
|
||||||
|
|
||||||
return (files.ElementAtOrDefault(nextIndex), isLastFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (var inputStream = GetInputStream(path, allowAsync))
|
|
||||||
{
|
|
||||||
if (seekFile)
|
|
||||||
{
|
|
||||||
TrySeek(inputStream, -20000);
|
|
||||||
}
|
|
||||||
|
|
||||||
await StreamHelper.CopyToAsync(
|
|
||||||
inputStream,
|
|
||||||
stream,
|
|
||||||
IODefaults.CopyToBufferSize,
|
|
||||||
emptyReadLimit,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TrySeek(FileStream stream, long offset)
|
|
||||||
{
|
{
|
||||||
if (!stream.CanSeek)
|
if (!stream.CanSeek)
|
||||||
{
|
{
|
||||||
|
@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
|
numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
|
||||||
|
|
||||||
if (!IsValidChannelNumber(numberString))
|
if (!IsValidChannelNumber(numberString))
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@ -55,39 +54,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
|
||||||
|
|
||||||
var typeName = GetType().Name;
|
var typeName = GetType().Name;
|
||||||
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
|
Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
|
||||||
|
|
||||||
// Response stream is disposed manually.
|
// Response stream is disposed manually.
|
||||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
|
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var extension = "ts";
|
|
||||||
var requiresRemux = false;
|
|
||||||
|
|
||||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
|
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
|
||||||
if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
|
if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
requiresRemux = true;
|
// Close the stream without any sharing features
|
||||||
}
|
response.Dispose();
|
||||||
else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
|
return;
|
||||||
contentType.IndexOf("dash", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
contentType.IndexOf("mpegURL", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
contentType.IndexOf("text/", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
requiresRemux = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the stream without any sharing features
|
SetTempFilePath("ts");
|
||||||
if (requiresRemux)
|
|
||||||
{
|
|
||||||
using (response)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SetTempFilePath(extension);
|
|
||||||
|
|
||||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
@ -117,16 +103,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
|
|
||||||
if (!taskCompletionSource.Task.Result)
|
if (!taskCompletionSource.Task.Result)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
|
Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
|
||||||
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
|
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetFilePath()
|
|
||||||
{
|
|
||||||
return TempFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Task.Run(
|
return Task.Run(
|
||||||
@ -134,10 +115,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
|
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
|
||||||
using var message = response;
|
using var message = response;
|
||||||
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(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
|
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
await StreamHelper.CopyToAsync(
|
await StreamHelper.CopyToAsync(
|
||||||
stream,
|
stream,
|
||||||
fileStream,
|
fileStream,
|
||||||
@ -147,19 +128,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
|
Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
|
||||||
openTaskCompletionSource.TrySetException(ex);
|
openTaskCompletionSource.TrySetException(ex);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
|
Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath);
|
||||||
openTaskCompletionSource.TrySetException(ex);
|
openTaskCompletionSource.TrySetException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
openTaskCompletionSource.TrySetResult(false);
|
openTaskCompletionSource.TrySetResult(false);
|
||||||
|
|
||||||
EnableStreamSharing = false;
|
EnableStreamSharing = false;
|
||||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
|
||||||
},
|
},
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Albums": "البومات",
|
"Albums": "ألبومات",
|
||||||
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
||||||
"Application": "تطبيق",
|
"Application": "تطبيق",
|
||||||
"Artists": "الفنانين",
|
"Artists": "الفنانين",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
|
"CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
|
||||||
"Channels": "القنوات",
|
"Channels": "القنوات",
|
||||||
"ChapterNameValue": "الفصل {0}",
|
"ChapterNameValue": "الفصل {0}",
|
||||||
"Collections": "مجموعات",
|
"Collections": "التجميعات",
|
||||||
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
||||||
"DeviceOnlineWithName": "{0} متصل",
|
"DeviceOnlineWithName": "{0} متصل",
|
||||||
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
|
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
|
||||||
|
@ -118,5 +118,7 @@
|
|||||||
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
|
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
|
||||||
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
|
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
|
||||||
"Undefined": "Indéfini",
|
"Undefined": "Indéfini",
|
||||||
"Forced": "Forcé"
|
"Forced": "Forcé",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Compacte la base de données et tronque l'espace libre. Lancer cette tâche après avoir scanné la bibliothèque ou faire d'autres changements impliquant des modifications de la base peuvent ameliorer les performances.",
|
||||||
|
"TaskOptimizeDatabase": "Optimiser la base de données"
|
||||||
}
|
}
|
||||||
|
@ -105,8 +105,8 @@
|
|||||||
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
||||||
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
|
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
|
||||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||||
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
|
"TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
|
||||||
"TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
|
"TaskRefreshLibrary": "Scanner la médiathèque",
|
||||||
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
|
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
|
||||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||||
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
|
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||||
"HeaderFavoriteAlbums": "Álbunes Favoritos",
|
"HeaderFavoriteAlbums": "Álbunes Favoritos",
|
||||||
"HeaderContinueWatching": "Seguir mirando",
|
"HeaderContinueWatching": "Seguir mirando",
|
||||||
"HeaderAlbumArtists": "Artistas de Album",
|
"HeaderAlbumArtists": "Artistas do Album",
|
||||||
"Genres": "Xéneros",
|
"Genres": "Xéneros",
|
||||||
"Forced": "Forzado",
|
"Forced": "Forzado",
|
||||||
"Folders": "Cartafoles",
|
"Folders": "Cartafoles",
|
||||||
@ -117,5 +117,7 @@
|
|||||||
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
|
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
|
||||||
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
|
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
|
||||||
"UserOnlineFromDevice": "{0} está en liña desde {1}",
|
"UserOnlineFromDevice": "{0} está en liña desde {1}",
|
||||||
"UserOfflineFromDevice": "{0} desconectouse desde {1}"
|
"UserOfflineFromDevice": "{0} desconectouse desde {1}",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
|
||||||
|
"TaskOptimizeDatabase": "Optimizar base de datos"
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"Favorites": "Favoriti",
|
"Favorites": "Favoriti",
|
||||||
"Folders": "Mape",
|
"Folders": "Mape",
|
||||||
"Genres": "Žanrovi",
|
"Genres": "Žanrovi",
|
||||||
"HeaderAlbumArtists": "Izvođači na albumu",
|
"HeaderAlbumArtists": "Album od izvođača",
|
||||||
"HeaderContinueWatching": "Nastavi gledati",
|
"HeaderContinueWatching": "Nastavi gledati",
|
||||||
"HeaderFavoriteAlbums": "Omiljeni albumi",
|
"HeaderFavoriteAlbums": "Omiljeni albumi",
|
||||||
"HeaderFavoriteArtists": "Omiljeni izvođači",
|
"HeaderFavoriteArtists": "Omiljeni izvođači",
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
"NameSeasonUnknown": "Sezon i panjohur",
|
"NameSeasonUnknown": "Sezon i panjohur",
|
||||||
"NameSeasonNumber": "Sezoni {0}",
|
"NameSeasonNumber": "Sezoni {0}",
|
||||||
"NameInstallFailed": "Instalimi i {0} dështoi",
|
"NameInstallFailed": "Instalimi i {0} dështoi",
|
||||||
"MusicVideos": "Videot muzikore",
|
"MusicVideos": "Video muzikore",
|
||||||
"Music": "Muzikë",
|
"Music": "Muzikë",
|
||||||
"Movies": "Filmat",
|
"Movies": "Filmat",
|
||||||
"MixedContent": "Përmbajtje e përzier",
|
"MixedContent": "Përmbajtje e përzier",
|
||||||
@ -96,7 +96,7 @@
|
|||||||
"HeaderFavoriteArtists": "Artistët e preferuar",
|
"HeaderFavoriteArtists": "Artistët e preferuar",
|
||||||
"HeaderFavoriteAlbums": "Albumet e preferuar",
|
"HeaderFavoriteAlbums": "Albumet e preferuar",
|
||||||
"HeaderContinueWatching": "Vazhdo të shikosh",
|
"HeaderContinueWatching": "Vazhdo të shikosh",
|
||||||
"HeaderAlbumArtists": "Artistët e albumeve",
|
"HeaderAlbumArtists": "Artistët e Albumeve",
|
||||||
"Genres": "Zhanret",
|
"Genres": "Zhanret",
|
||||||
"Folders": "Skedarët",
|
"Folders": "Skedarët",
|
||||||
"Favorites": "Të preferuarat",
|
"Favorites": "Të preferuarat",
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
"NameSeasonUnknown": "نامعلوم باب",
|
"NameSeasonUnknown": "نامعلوم باب",
|
||||||
"NameSeasonNumber": "باب {0}",
|
"NameSeasonNumber": "باب {0}",
|
||||||
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
|
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
|
||||||
"MusicVideos": "موسیقی ویڈیو",
|
"MusicVideos": "ویڈیو موسیقی",
|
||||||
"Music": "موسیقی",
|
"Music": "موسیقی",
|
||||||
"MixedContent": "مخلوط مواد",
|
"MixedContent": "مخلوط مواد",
|
||||||
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
|
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
|
||||||
@ -99,18 +99,19 @@
|
|||||||
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
|
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
|
||||||
"Latest": "تازہ ترین",
|
"Latest": "تازہ ترین",
|
||||||
"LabelRunningTimeValue": "چلانے کی مدت",
|
"LabelRunningTimeValue": "چلانے کی مدت",
|
||||||
"LabelIpAddressValue": "ای پی پتے {0}",
|
"LabelIpAddressValue": "آئ پی ایڈریس {0}",
|
||||||
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
|
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
|
||||||
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
|
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
|
||||||
"Inherit": "وراثت میں",
|
"Inherit": "وراثت میں",
|
||||||
"HomeVideos": "ہوم ویڈیو",
|
"HomeVideos": "ہوم ویڈیو",
|
||||||
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
|
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
|
||||||
"FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
|
"FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
|
||||||
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
|
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
|
||||||
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
|
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
|
||||||
"ChapterNameValue": "باب",
|
"ChapterNameValue": "باب",
|
||||||
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
|
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
|
||||||
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
|
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
|
||||||
"Application": "پروگرام",
|
"Application": "پروگرام",
|
||||||
"AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
|
"AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
|
||||||
|
"Forced": "جَبری"
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,15 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
Name = "Playlists";
|
Name = "Playlists";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public override bool IsHidden => true;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public override bool SupportsInheritedParentImages => false;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user) && GetChildren(user, true).Any();
|
return base.IsVisible(user) && GetChildren(user, true).Any();
|
||||||
@ -27,15 +36,6 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
|
return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool IsHidden => true;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override bool SupportsInheritedParentImages => false;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
|
|
||||||
|
|
||||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.User == null)
|
if (query.User == null)
|
||||||
|
@ -8,10 +8,10 @@ using System.Reflection;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using Jellyfin.Extensions.Json.Converters;
|
using Jellyfin.Extensions.Json.Converters;
|
||||||
|
using MediaBrowser.Common;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
@ -39,14 +39,6 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
|
|
||||||
private IHttpClientFactory? _httpClientFactory;
|
private IHttpClientFactory? _httpClientFactory;
|
||||||
|
|
||||||
private IHttpClientFactory HttpClientFactory
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve<IHttpClientFactory>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -86,6 +78,14 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
|
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IHttpClientFactory HttpClientFactory
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _httpClientFactory ??= _appHost.Resolve<IHttpClientFactory>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Plugins.
|
/// Gets the Plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Quick connect implementation.
|
/// Quick connect implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class QuickConnectManager : IQuickConnect, IDisposable
|
public class QuickConnectManager : IQuickConnect
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The length of user facing codes.
|
/// The length of user facing codes.
|
||||||
@ -30,7 +30,6 @@ namespace Emby.Server.Implementations.QuickConnect
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const int Timeout = 10;
|
private const int Timeout = 10;
|
||||||
|
|
||||||
private readonly RNGCryptoServiceProvider _rng = new ();
|
|
||||||
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
|
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
|
||||||
private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
|
private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
|
||||||
|
|
||||||
@ -140,7 +139,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
|||||||
uint scale = uint.MaxValue;
|
uint scale = uint.MaxValue;
|
||||||
while (scale == uint.MaxValue)
|
while (scale == uint.MaxValue)
|
||||||
{
|
{
|
||||||
_rng.GetBytes(raw);
|
RandomNumberGenerator.Fill(raw);
|
||||||
scale = BitConverter.ToUInt32(raw);
|
scale = BitConverter.ToUInt32(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,31 +198,10 @@ namespace Emby.Server.Implementations.QuickConnect
|
|||||||
return result.AuthenticationResult;
|
return result.AuthenticationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">Dispose unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_rng.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GenerateSecureRandom(int length = 32)
|
private string GenerateSecureRandom(int length = 32)
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[length];
|
Span<byte> bytes = stackalloc byte[length];
|
||||||
_rng.GetBytes(bytes);
|
RandomNumberGenerator.Fill(bytes);
|
||||||
|
|
||||||
return Convert.ToHexString(bytes);
|
return Convert.ToHexString(bytes);
|
||||||
}
|
}
|
||||||
|
@ -28,16 +28,6 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
throw new ArgumentNullException(nameof(y));
|
throw new ArgumentNullException(nameof(y));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x.PremiereDate.HasValue && y.PremiereDate.HasValue)
|
|
||||||
{
|
|
||||||
var val = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
|
|
||||||
|
|
||||||
if (val != 0)
|
|
||||||
{
|
|
||||||
// return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var episode1 = x as Episode;
|
var episode1 = x as Episode;
|
||||||
var episode2 = y as Episode;
|
var episode2 = y as Episode;
|
||||||
|
|
||||||
@ -156,8 +146,14 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
{
|
{
|
||||||
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
||||||
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
||||||
|
var comparisonResult = xValue.CompareTo(yValue);
|
||||||
|
// If equal, compare premiere dates
|
||||||
|
if (comparisonResult == 0 && x.PremiereDate.HasValue && y.PremiereDate.HasValue)
|
||||||
|
{
|
||||||
|
comparisonResult = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
return xValue.CompareTo(yValue);
|
return comparisonResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
|
||||||
{
|
{
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrDefaultRequirement);
|
context.Succeed(requirement);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validated = ValidateClaims(context.User);
|
var validated = ValidateClaims(context.User);
|
||||||
if (validated)
|
if (validated)
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrDefaultRequirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -33,18 +33,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement firstTimeSetupOrElevatedRequirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
|
||||||
{
|
{
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
context.Succeed(requirement);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validated = ValidateClaims(context.User);
|
var validated = ValidateClaims(context.User);
|
||||||
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||||
|
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
||||||
if (user.ProfileImage != null)
|
if (user.ProfileImage != null)
|
||||||
{
|
{
|
||||||
@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||||
|
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
||||||
if (user.ProfileImage != null)
|
if (user.ProfileImage != null)
|
||||||
{
|
{
|
||||||
@ -341,7 +341,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||||
|
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -377,7 +377,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||||
|
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -2026,7 +2026,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhysicalFile(imagePath, imageContentType);
|
return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1199,15 +1199,15 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
||||||
{
|
{
|
||||||
var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
|
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||||
if (liveStreamInfo == null)
|
if (liveStreamInfo == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
|
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using System.Net.Http;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
@ -199,13 +200,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
|
throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
var ext = response.Content.Headers.ContentType.MediaType.AsSpan().RightPart('/').ToString();
|
||||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||||
|
|
||||||
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||||
Directory.CreateDirectory(fullCacheDirectory);
|
Directory.CreateDirectory(fullCacheDirectory);
|
||||||
// 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 fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
|
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
|
|
||||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||||
|
@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
// For older files, assume fully static
|
// For older files, assume fully static
|
||||||
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
|
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
|
||||||
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
|
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
return File(stream, "text/plain; charset=utf-8");
|
return File(stream, "text/plain; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,10 +21,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public ActionResult<UtcTimeResponse> GetUtcTime()
|
public ActionResult<UtcTimeResponse> GetUtcTime()
|
||||||
{
|
{
|
||||||
// Important to keep the following line at the beginning
|
// Important to keep the following line at the beginning
|
||||||
var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
|
var requestReceptionTime = DateTime.UtcNow;
|
||||||
|
|
||||||
// Important to keep the following line at the end
|
// Important to keep the following line at the end
|
||||||
var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
|
var responseTransmissionTime = DateTime.UtcNow;
|
||||||
|
|
||||||
// Implementing NTP on such a high level results in this useless
|
// Implementing NTP on such a high level results in this useless
|
||||||
// information being sent. On the other hand it enables future additions.
|
// information being sent. On the other hand it enables future additions.
|
||||||
|
@ -147,7 +147,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
? _userManager.GetUserById(userId.Value)
|
? _userManager.GetUserById(userId.Value)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
|
var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
|
||||||
|
|
||||||
var parentIdGuid = parentId ?? Guid.Empty;
|
var parentIdGuid = parentId ?? Guid.Empty;
|
||||||
|
|
||||||
|
@ -453,14 +453,15 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||||
|
|
||||||
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
|
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
|
||||||
|
if (liveStreamInfo == null)
|
||||||
{
|
{
|
||||||
AllowEndOfFile = false
|
return NotFound();
|
||||||
}.WriteToAsync(Response.Body, CancellationToken.None)
|
}
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
|
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||||
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
||||||
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
|
return File(liveStream, MimeTypes.GetMimeType("file.ts")!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static remote stream
|
// Static remote stream
|
||||||
@ -492,13 +493,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (state.MediaSource.IsInfiniteStream)
|
if (state.MediaSource.IsInfiniteStream)
|
||||||
{
|
{
|
||||||
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
|
var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
|
||||||
{
|
return File(liveStream, contentType);
|
||||||
AllowEndOfFile = false
|
|
||||||
}.WriteToAsync(Response.Body, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return File(Response.Body, contentType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(
|
return FileStreamResponseHelpers.GetStaticFileResult(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Net.Http;
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Models.StreamingDtos;
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
@ -120,14 +121,15 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
|
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
|
||||||
|
|
||||||
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
|
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
|
||||||
{
|
if (liveStreamInfo == null)
|
||||||
AllowEndOfFile = false
|
{
|
||||||
}.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
|
throw new FileNotFoundException();
|
||||||
.ConfigureAwait(false);
|
}
|
||||||
|
|
||||||
|
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||||
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
||||||
return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!);
|
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static remote stream
|
// Static remote stream
|
||||||
@ -159,13 +161,8 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
if (state.MediaSource.IsInfiniteStream)
|
if (state.MediaSource.IsInfiniteStream)
|
||||||
{
|
{
|
||||||
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
|
var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
|
||||||
{
|
return new FileStreamResult(stream, contentType);
|
||||||
AllowEndOfFile = false
|
|
||||||
}.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(
|
return FileStreamResponseHelpers.GetStaticFileResult(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Models.PlaybackDtos;
|
using Jellyfin.Api.Models.PlaybackDtos;
|
||||||
@ -40,7 +41,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
// Can't dispose the response as it's required up the call chain.
|
// Can't dispose the response as it's required up the call chain.
|
||||||
var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
|
var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
|
||||||
var contentType = response.Content.Headers.ContentType?.ToString();
|
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
|
||||||
|
|
||||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
FileAccess.Read,
|
FileAccess.Read,
|
||||||
FileShare.ReadWrite,
|
FileShare.ReadWrite,
|
||||||
IODefaults.FileStreamBufferSize,
|
IODefaults.FileStreamBufferSize,
|
||||||
(AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan);
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
await using (fileStream.ConfigureAwait(false))
|
await using (fileStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(fileStream);
|
using var reader = new StreamReader(fileStream);
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Api.Models.PlaybackDtos;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Progressive file copier.
|
|
||||||
/// </summary>
|
|
||||||
public class ProgressiveFileCopier
|
|
||||||
{
|
|
||||||
private readonly TranscodingJobDto? _job;
|
|
||||||
private readonly string? _path;
|
|
||||||
private readonly CancellationToken _cancellationToken;
|
|
||||||
private readonly IDirectStreamProvider? _directStreamProvider;
|
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
|
||||||
private long _bytesWritten;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path to copy from.</param>
|
|
||||||
/// <param name="job">The transcoding job.</param>
|
|
||||||
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_path = path;
|
|
||||||
_job = job;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
|
|
||||||
/// <param name="job">The transcoding job.</param>
|
|
||||||
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_directStreamProvider = directStreamProvider;
|
|
||||||
_job = job;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether allow read end of file.
|
|
||||||
/// </summary>
|
|
||||||
public bool AllowEndOfFile { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets copy start position.
|
|
||||||
/// </summary>
|
|
||||||
public long StartPosition { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Write source stream to output.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputStream">Output stream.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>A <see cref="Task"/>.</returns>
|
|
||||||
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken);
|
|
||||||
cancellationToken = linkedCancellationTokenSource.Token;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_directStreamProvider != null)
|
|
||||||
{
|
|
||||||
await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileOptions = FileOptions.SequentialScan;
|
|
||||||
var allowAsyncFileRead = false;
|
|
||||||
|
|
||||||
if (AsyncFile.UseAsyncIO)
|
|
||||||
{
|
|
||||||
fileOptions |= FileOptions.Asynchronous;
|
|
||||||
allowAsyncFileRead = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_path == null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException(nameof(_path));
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
|
|
||||||
|
|
||||||
var eofCount = 0;
|
|
||||||
const int EmptyReadLimit = 20;
|
|
||||||
if (StartPosition > 0)
|
|
||||||
{
|
|
||||||
inputStream.Position = StartPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (eofCount < EmptyReadLimit || !AllowEndOfFile)
|
|
||||||
{
|
|
||||||
var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
if (_job == null || _job.HasExited)
|
|
||||||
{
|
|
||||||
eofCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
eofCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
_transcodingJobHelper.OnTranscodeEndRequest(_job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int bytesRead;
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
|
|
||||||
if (readAsync)
|
|
||||||
{
|
|
||||||
bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bytesRead = source.Read(array, 0, array.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (bytesRead != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = bytesRead;
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_bytesWritten += bytesRead;
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readAsync)
|
|
||||||
{
|
|
||||||
bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bytesRead = source.Read(array, 0, array.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,11 +13,10 @@ namespace Jellyfin.Api.Helpers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProgressiveFileStream : Stream
|
public class ProgressiveFileStream : Stream
|
||||||
{
|
{
|
||||||
private readonly FileStream _fileStream;
|
private readonly Stream _stream;
|
||||||
private readonly TranscodingJobDto? _job;
|
private readonly TranscodingJobDto? _job;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
private readonly TranscodingJobHelper? _transcodingJobHelper;
|
||||||
private readonly int _timeoutMs;
|
private readonly int _timeoutMs;
|
||||||
private readonly bool _allowAsyncFileRead;
|
|
||||||
private int _bytesWritten;
|
private int _bytesWritten;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
@ -33,23 +32,25 @@ namespace Jellyfin.Api.Helpers
|
|||||||
_job = job;
|
_job = job;
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
_timeoutMs = timeoutMs;
|
_timeoutMs = timeoutMs;
|
||||||
_bytesWritten = 0;
|
|
||||||
|
|
||||||
var fileOptions = FileOptions.SequentialScan;
|
_stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
_allowAsyncFileRead = false;
|
}
|
||||||
|
|
||||||
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
|
/// <summary>
|
||||||
if (AsyncFile.UseAsyncIO)
|
/// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
|
||||||
{
|
/// </summary>
|
||||||
fileOptions |= FileOptions.Asynchronous;
|
/// <param name="stream">The stream to progressively copy.</param>
|
||||||
_allowAsyncFileRead = true;
|
/// <param name="timeoutMs">The timeout duration in milliseconds.</param>
|
||||||
}
|
public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
|
||||||
|
{
|
||||||
_fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
|
_job = null;
|
||||||
|
_transcodingJobHelper = null;
|
||||||
|
_timeoutMs = timeoutMs;
|
||||||
|
_stream = stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanRead => _fileStream.CanRead;
|
public override bool CanRead => _stream.CanRead;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanSeek => false;
|
public override bool CanSeek => false;
|
||||||
@ -70,13 +71,13 @@ namespace Jellyfin.Api.Helpers
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
{
|
{
|
||||||
_fileStream.Flush();
|
_stream.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
{
|
{
|
||||||
return _fileStream.Read(buffer, offset, count);
|
return _stream.Read(buffer, offset, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -90,15 +91,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
while (remainingBytesToRead > 0)
|
while (remainingBytesToRead > 0)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
int bytesRead;
|
int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
|
||||||
if (_allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingBytesToRead -= bytesRead;
|
remainingBytesToRead -= bytesRead;
|
||||||
newOffset += bytesRead;
|
newOffset += bytesRead;
|
||||||
@ -152,11 +145,11 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_fileStream.Dispose();
|
_stream.Dispose();
|
||||||
|
|
||||||
if (_job != null)
|
if (_job != null)
|
||||||
{
|
{
|
||||||
_transcodingJobHelper.OnTranscodeEndRequest(_job);
|
_transcodingJobHelper?.OnTranscodeEndRequest(_job);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Models.StreamingDtos;
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@ -81,7 +82,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
throw new ResourceNotFoundException(nameof(httpRequest.Path));
|
throw new ResourceNotFoundException(nameof(httpRequest.Path));
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = httpRequest.Path.Value.Split('.')[^1];
|
var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
|
if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
|
||||||
{
|
{
|
||||||
|
@ -557,7 +557,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
|
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
|
||||||
|
|
||||||
// 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.
|
||||||
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
|
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
|
|
||||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||||
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
|
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<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>
|
||||||
@ -14,7 +14,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.1" />
|
||||||
|
@ -32,7 +32,8 @@ namespace Jellyfin.Api.ModelBinders
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue);
|
// REVIEW: This shouldn't be null here
|
||||||
|
var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue!);
|
||||||
bindingContext.Result = ModelBindingResult.Success(convertedValue);
|
bindingContext.Result = ModelBindingResult.Success(convertedValue);
|
||||||
}
|
}
|
||||||
catch (FormatException e)
|
catch (FormatException e)
|
||||||
|
@ -60,6 +60,9 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the direct stream provicer.
|
/// Gets or sets the direct stream provicer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Deprecated.
|
||||||
|
/// </remarks>
|
||||||
public IDirectStreamProvider? DirectStreamProvider { get; set; }
|
public IDirectStreamProvider? DirectStreamProvider { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<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>
|
||||||
|
@ -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>
|
||||||
@ -28,11 +28,6 @@
|
|||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<!-- Needed for https://github.com/dotnet/roslyn-analyzers/issues/4382 which is in the SDK yet -->
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Code analysers-->
|
<!-- Code analysers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@ -19,13 +19,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.10">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -23,7 +23,7 @@ namespace Jellyfin.Server.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName)
|
public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string? policyName)
|
||||||
{
|
{
|
||||||
var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
|
var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
|
||||||
var builder = new CorsPolicyBuilder()
|
var builder = new CorsPolicyBuilder()
|
||||||
@ -43,7 +43,7 @@ namespace Jellyfin.Server.Configuration
|
|||||||
.AllowCredentials();
|
.AllowCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(builder.Build());
|
return Task.FromResult<CorsPolicy?>(builder.Build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,7 +278,7 @@ namespace Jellyfin.Server.Extensions
|
|||||||
{
|
{
|
||||||
Type = SecuritySchemeType.ApiKey,
|
Type = SecuritySchemeType.ApiKey,
|
||||||
In = ParameterLocation.Header,
|
In = ParameterLocation.Header,
|
||||||
Name = "X-Emby-Authorization",
|
Name = "Authorization",
|
||||||
Description = "API key header parameter"
|
Description = "API key header parameter"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
// The MIT License (MIT)
|
||||||
|
//
|
||||||
|
// Copyright (c) .NET Foundation and Contributors
|
||||||
|
//
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Infrastructure
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
|
public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override FileMetadata GetFileInfo(string path)
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
var length = fileInfo.Length;
|
||||||
|
// This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
|
||||||
|
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||||
|
{
|
||||||
|
using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
length = RandomAccess.GetLength(fileHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileMetadata
|
||||||
|
{
|
||||||
|
Exists = fileInfo.Exists,
|
||||||
|
Length = length,
|
||||||
|
LastModified = fileInfo.LastWriteTimeUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range != null && rangeLength == 0)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
|
||||||
|
if (!IsSymLink(result.FileName))
|
||||||
|
{
|
||||||
|
return base.WriteFileAsync(context, result, range, rangeLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = context.HttpContext.Response;
|
||||||
|
|
||||||
|
if (range != null)
|
||||||
|
{
|
||||||
|
return SendFileAsync(
|
||||||
|
result.FileName,
|
||||||
|
response,
|
||||||
|
offset: range.From ?? 0L,
|
||||||
|
count: rangeLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendFileAsync(
|
||||||
|
result.FileName,
|
||||||
|
response,
|
||||||
|
offset: 0,
|
||||||
|
count: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
|
||||||
|
{
|
||||||
|
var fileInfo = GetFileInfo(filePath);
|
||||||
|
if (offset < 0 || offset > fileInfo.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count.HasValue
|
||||||
|
&& (count.Value < 0 || count.Value > fileInfo.Length - offset))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from SendFileFallback.SendFileAsync
|
||||||
|
const int BufferSize = 1024 * 16;
|
||||||
|
|
||||||
|
await using var fileStream = new FileStream(
|
||||||
|
filePath,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.Read,
|
||||||
|
FileShare.ReadWrite,
|
||||||
|
bufferSize: BufferSize,
|
||||||
|
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
|
|
||||||
|
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||||
|
await StreamCopyOperation
|
||||||
|
.CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
|
||||||
|
.ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>jellyfin</AssemblyName>
|
<AssemblyName>jellyfin</AssemblyName>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
@ -33,8 +33,8 @@
|
|||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.10" />
|
||||||
<PackageReference Include="prometheus-net" Version="5.0.1" />
|
<PackageReference Include="prometheus-net" Version="5.0.1" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.1" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||||
|
@ -27,7 +27,11 @@ namespace Jellyfin.Server.Middleware
|
|||||||
/// <returns>The async task.</returns>
|
/// <returns>The async task.</returns>
|
||||||
public async Task Invoke(HttpContext httpContext)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
|
var feature = httpContext.Features.Get<IQueryFeature>();
|
||||||
|
if (feature != null)
|
||||||
|
{
|
||||||
|
httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature));
|
||||||
|
}
|
||||||
|
|
||||||
await _next(httpContext).ConfigureAwait(false);
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -52,20 +52,14 @@ namespace Jellyfin.Server.Middleware
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unencode and re-parse querystring.
|
if (!key.Contains('='))
|
||||||
var unencodedKey = HttpUtility.UrlDecode(key);
|
|
||||||
|
|
||||||
if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
// Don't do anything if it's not encoded.
|
|
||||||
_store = value;
|
_store = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pairs = new Dictionary<string, StringValues>();
|
var pairs = new Dictionary<string, StringValues>();
|
||||||
var queryString = unencodedKey.SpanSplit('&');
|
foreach (var pair in key.SpanSplit('&'))
|
||||||
|
|
||||||
foreach (var pair in queryString)
|
|
||||||
{
|
{
|
||||||
var i = pair.IndexOf('=');
|
var i = pair.IndexOf('=');
|
||||||
if (i == -1)
|
if (i == -1)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Emby.Server.Implementations.Data;
|
using Emby.Server.Implementations.Data;
|
||||||
using Emby.Server.Implementations.Serialization;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
@ -10,6 +9,7 @@ using Jellyfin.Server.Implementations.Users;
|
|||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Users;
|
using MediaBrowser.Model.Users;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
using SQLitePCL.pretty;
|
||||||
@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
private readonly ILogger<MigrateUserDb> _logger;
|
private readonly ILogger<MigrateUserDb> _logger;
|
||||||
private readonly IServerApplicationPaths _paths;
|
private readonly IServerApplicationPaths _paths;
|
||||||
private readonly JellyfinDbProvider _provider;
|
private readonly JellyfinDbProvider _provider;
|
||||||
private readonly MyXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
|
/// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
|
||||||
@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
ILogger<MigrateUserDb> logger,
|
ILogger<MigrateUserDb> logger,
|
||||||
IServerApplicationPaths paths,
|
IServerApplicationPaths paths,
|
||||||
JellyfinDbProvider provider,
|
JellyfinDbProvider provider,
|
||||||
MyXmlSerializer xmlSerializer)
|
IXmlSerializer xmlSerializer)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_paths = paths;
|
_paths = paths;
|
||||||
|
@ -195,9 +195,9 @@ namespace Jellyfin.Server
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await webHost.StartAsync().ConfigureAwait(false);
|
await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex) when (ex is not TaskCanceledException)
|
||||||
{
|
{
|
||||||
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
|
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
|
||||||
throw;
|
throw;
|
||||||
@ -547,7 +547,7 @@ namespace Jellyfin.Server
|
|||||||
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
|
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
|
||||||
|
|
||||||
// Copy the resource contents to the expected file path for the config file
|
// Copy the resource contents to the expected file path for the config file
|
||||||
await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
|
await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
await resource.CopyToAsync(dst).ConfigureAwait(false);
|
await resource.CopyToAsync(dst).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using System.Text;
|
|||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
using Jellyfin.Server.Extensions;
|
using Jellyfin.Server.Extensions;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
|
using Jellyfin.Server.Infrastructure;
|
||||||
using Jellyfin.Server.Middleware;
|
using Jellyfin.Server.Middleware;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
@ -14,6 +15,8 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Extensions;
|
using MediaBrowser.Controller.Extensions;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -56,6 +59,9 @@ namespace Jellyfin.Server
|
|||||||
{
|
{
|
||||||
options.HttpsPort = _serverApplicationHost.HttpsPort;
|
options.HttpsPort = _serverApplicationHost.HttpsPort;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||||
|
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||||
|
|
||||||
services.AddJellyfinApiSwagger();
|
services.AddJellyfinApiSwagger();
|
||||||
|
@ -29,7 +29,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>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
@ -74,6 +74,6 @@ namespace MediaBrowser.Controller.Dlna
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filename">The filename.</param>
|
/// <param name="filename">The filename.</param>
|
||||||
/// <returns>DlnaIconResponse.</returns>
|
/// <returns>DlnaIconResponse.</returns>
|
||||||
ImageStream GetIcon(string filename);
|
ImageStream? GetIcon(string filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
/// <returns>Guid.</returns>
|
/// <returns>Guid.</returns>
|
||||||
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
|
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
|
||||||
|
|
||||||
string GetImageCacheTag(BaseItem item, ChapterInfo info);
|
string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
|
||||||
|
|
||||||
string? GetImageCacheTag(User user);
|
string? GetImageCacheTag(User user);
|
||||||
|
|
||||||
|
@ -8,11 +8,16 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
{
|
{
|
||||||
public class ImageStream : IDisposable
|
public class ImageStream : IDisposable
|
||||||
{
|
{
|
||||||
|
public ImageStream(Stream stream)
|
||||||
|
{
|
||||||
|
Stream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the stream.
|
/// Gets the stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The stream.</value>
|
/// <value>The stream.</value>
|
||||||
public Stream? Stream { get; set; }
|
public Stream Stream { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the format.
|
/// Gets or sets the format.
|
||||||
|
@ -44,18 +44,10 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The trailer folder name.
|
/// The trailer folder name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string TrailerFolderName = "trailers";
|
public const string TrailersFolderName = "trailers";
|
||||||
public const string ThemeSongsFolderName = "theme-music";
|
public const string ThemeSongsFolderName = "theme-music";
|
||||||
public const string ThemeSongFilename = "theme";
|
public const string ThemeSongFileName = "theme";
|
||||||
public const string ThemeVideosFolderName = "backdrops";
|
public const string ThemeVideosFolderName = "backdrops";
|
||||||
public const string ExtrasFolderName = "extras";
|
|
||||||
public const string BehindTheScenesFolderName = "behind the scenes";
|
|
||||||
public const string DeletedScenesFolderName = "deleted scenes";
|
|
||||||
public const string InterviewFolderName = "interviews";
|
|
||||||
public const string SceneFolderName = "scenes";
|
|
||||||
public const string SampleFolderName = "samples";
|
|
||||||
public const string ShortsFolderName = "shorts";
|
|
||||||
public const string FeaturettesFolderName = "featurettes";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The supported image extensions.
|
/// The supported image extensions.
|
||||||
@ -93,16 +85,20 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
};
|
};
|
||||||
|
|
||||||
public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
|
public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
|
||||||
public static readonly string[] AllExtrasTypesFolderNames =
|
|
||||||
|
/// <summary>
|
||||||
|
/// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Dictionary<string, ExtraType> AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
ExtrasFolderName,
|
["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown,
|
||||||
BehindTheScenesFolderName,
|
["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes,
|
||||||
DeletedScenesFolderName,
|
["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene,
|
||||||
InterviewFolderName,
|
["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview,
|
||||||
SceneFolderName,
|
["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene,
|
||||||
SampleFolderName,
|
["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample,
|
||||||
ShortsFolderName,
|
["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip,
|
||||||
FeaturettesFolderName
|
["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip
|
||||||
};
|
};
|
||||||
|
|
||||||
private string _sortName;
|
private string _sortName;
|
||||||
@ -1358,7 +1354,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
// Support plex/xbmc convention
|
// Support plex/xbmc convention
|
||||||
files.AddRange(fileSystemChildren
|
files.AddRange(fileSystemChildren
|
||||||
.Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
|
.Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFileName, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
||||||
.OfType<Audio.Audio>()
|
.OfType<Audio.Audio>()
|
||||||
@ -1417,39 +1413,24 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
var extras = new List<Video>();
|
return fileSystemChildren
|
||||||
|
.Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name))
|
||||||
var libraryOptions = new LibraryOptions();
|
.SelectMany(folder => LibraryManager
|
||||||
var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList();
|
.ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions())
|
||||||
foreach (var extraFolderName in AllExtrasTypesFolderNames)
|
|
||||||
{
|
|
||||||
var files = folders
|
|
||||||
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.SelectMany(i => FileSystem.GetFiles(i.FullName));
|
|
||||||
|
|
||||||
// Re-using the same instance of LibraryOptions since it looks like it's never being altered.
|
|
||||||
extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions)
|
|
||||||
.OfType<Video>()
|
.OfType<Video>()
|
||||||
.Select(item =>
|
.Select(video =>
|
||||||
{
|
{
|
||||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||||
if (LibraryManager.GetItemById(item.Id) is Video dbItem)
|
if (LibraryManager.GetItemById(video.Id) is Video dbItem)
|
||||||
{
|
{
|
||||||
item = dbItem;
|
video = dbItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use some hackery to get the extra type based on foldername
|
video.ExtraType = AllExtrasTypesFolderNames[folder.Name];
|
||||||
item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType)
|
return video;
|
||||||
? extraType
|
})
|
||||||
: Model.Entities.ExtraType.Unknown;
|
.OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes
|
||||||
|
.ToArray();
|
||||||
return item;
|
|
||||||
|
|
||||||
// Sort them so that the list can be easily compared for changes
|
|
||||||
}).OrderBy(i => i.Path));
|
|
||||||
}
|
|
||||||
|
|
||||||
return extras.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task RefreshMetadata(CancellationToken cancellationToken)
|
public Task RefreshMetadata(CancellationToken cancellationToken)
|
||||||
|
19
MediaBrowser.Controller/Library/IDirectStreamProvider.cs
Normal file
19
MediaBrowser.Controller/Library/IDirectStreamProvider.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Library
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The direct live TV stream provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Deprecated.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IDirectStreamProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the live stream, shared streams seek to the end of the file first.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The stream.</returns>
|
||||||
|
Stream GetStream();
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#pragma warning disable CA1711, CS1591
|
#pragma warning disable CA1711, CS1591
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
@ -25,5 +26,7 @@ namespace MediaBrowser.Controller.Library
|
|||||||
Task Open(CancellationToken openCancellationToken);
|
Task Open(CancellationToken openCancellationToken);
|
||||||
|
|
||||||
Task Close();
|
Task Close();
|
||||||
|
|
||||||
|
Stream GetStream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
@ -110,6 +109,20 @@ namespace MediaBrowser.Controller.Library
|
|||||||
|
|
||||||
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
|
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the live stream info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The identifier.</param>
|
||||||
|
/// <returns>An instance of <see cref="ILiveStream"/>.</returns>
|
||||||
|
public ILiveStream GetLiveStreamInfo(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the live stream info using the stream's unique id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uniqueId">The unique identifier.</param>
|
||||||
|
/// <returns>An instance of <see cref="ILiveStream"/>.</returns>
|
||||||
|
public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Closes the media source.
|
/// Closes the media source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -126,14 +139,5 @@ namespace MediaBrowser.Controller.Library
|
|||||||
void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
|
void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
|
||||||
|
|
||||||
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
|
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IDirectStreamProvider
|
|
||||||
{
|
|
||||||
Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
string GetFilePath();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,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>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
@ -541,7 +541,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return MimeType;
|
return MimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
|
if (enableStreamDefault)
|
||||||
|
{
|
||||||
|
return MimeTypes.GetMimeType(outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MimeTypes.GetMimeType(outputPath, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
|
public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
|
||||||
|
@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var size = part.Split('=', 2)[^1];
|
var size = part.Split('=', 2)[^1];
|
||||||
|
|
||||||
int? scale = null;
|
int? scale = null;
|
||||||
if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
|
if (size.Contains("kb", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
scale = 1024;
|
scale = 1024;
|
||||||
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
|
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
@ -139,7 +139,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var rate = part.Split('=', 2)[^1];
|
var rate = part.Split('=', 2)[^1];
|
||||||
|
|
||||||
int? scale = null;
|
int? scale = null;
|
||||||
if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
|
if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
scale = 1024;
|
scale = 1024;
|
||||||
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
|
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
@ -11,7 +11,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>
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@ -144,9 +145,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
if (!string.IsNullOrWhiteSpace(val))
|
||||||
{
|
{
|
||||||
if (DateTime.TryParse(val, out var added))
|
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
|
||||||
{
|
{
|
||||||
item.DateCreated = added.ToUniversalTime();
|
item.DateCreated = added;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -331,7 +332,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
{
|
{
|
||||||
if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out var runtime))
|
if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, _usCulture, out var runtime))
|
||||||
{
|
{
|
||||||
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
|
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
|
||||||
}
|
}
|
||||||
@ -534,9 +535,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(firstAired))
|
if (!string.IsNullOrWhiteSpace(firstAired))
|
||||||
{
|
{
|
||||||
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var airDate) && airDate.Year > 1850)
|
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
|
||||||
{
|
{
|
||||||
item.PremiereDate = airDate.ToUniversalTime();
|
item.PremiereDate = airDate;
|
||||||
item.ProductionYear = airDate.Year;
|
item.ProductionYear = airDate.Year;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -551,9 +552,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(firstAired))
|
if (!string.IsNullOrWhiteSpace(firstAired))
|
||||||
{
|
{
|
||||||
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var airDate) && airDate.Year > 1850)
|
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
|
||||||
{
|
{
|
||||||
item.EndDate = airDate.ToUniversalTime();
|
item.EndDate = airDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,14 +165,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
// User had cleared the custom path in UI
|
// User had cleared the custom path in UI
|
||||||
newPath = string.Empty;
|
newPath = string.Empty;
|
||||||
}
|
}
|
||||||
else if (Directory.Exists(path))
|
|
||||||
{
|
|
||||||
// Given path is directory, so resolve down to filename
|
|
||||||
newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newPath = path;
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
// Given path is directory, so resolve down to filename
|
||||||
|
newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!new EncoderValidator(_logger, newPath).ValidateVersion())
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the new ffmpeg path to the xml as <EncoderAppPath>
|
// Write the new ffmpeg path to the xml as <EncoderAppPath>
|
||||||
|
@ -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>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BDInfo" Version="0.7.6.1" />
|
<PackageReference Include="BDInfo" Version="0.7.6.1" />
|
||||||
<PackageReference Include="libse" Version="3.6.0" />
|
<PackageReference Include="libse" Version="3.6.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
|
||||||
<PackageReference Include="UTF.Unknown" Version="2.4.0" />
|
<PackageReference Include="UTF.Unknown" Version="2.4.0" />
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user