diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index e58a2bdc7e..31f861f63f 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
- default: 5.0.302
+ default: 6.0.x
jobs:
- job: CompatibilityCheck
@@ -34,6 +34,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker Tool'
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index d2c087c14d..1086d51d27 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 5.0.302
+ DotNetSdkVersion: 6.0.x
jobs:
- job: Build
@@ -54,6 +54,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Publish Server'
@@ -91,3 +92,10 @@ jobs:
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
+
+ - task: PublishPipelineArtifact@1
+ displayName: 'Publish Artifact Extensions'
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
+ artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 543fd7fc6d..4abe52b43b 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -195,10 +195,11 @@ jobs:
steps:
- task: UseDotNet@2
- displayName: 'Use .NET 5.0 sdk'
+ displayName: 'Use .NET 6.0 sdk'
inputs:
packageType: 'sdk'
- version: '5.0.x'
+ version: '6.0.x'
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
@@ -211,6 +212,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
@@ -225,6 +227,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 7ec4cdad1d..80a5732eee 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
- default: 5.0.302
+ default: 6.0.x
jobs:
- job: Test
@@ -41,6 +41,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud'
@@ -94,5 +95,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
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'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 4e8b6557b9..19c9caacbb 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -5,8 +5,6 @@ variables:
value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-- name: DotNetSdkVersion
- value: 5.0.302
pr:
autoCancel: true
@@ -57,6 +55,9 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
+ Extensions:
+ NugetPackageName: Jellyfin.Extensions
+ AssemblyFileName: Jellyfin.Extensions.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3e456f9093..e07d913b5a 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,7 +24,9 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: '5.0.x'
+ dotnet-version: '6.0.x'
+ include-prerelease: true
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/Dockerfile b/Dockerfile
index 791a6113ed..73b5908b4e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# 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
ARG JELLYFIN_WEB_VERSION=master
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 8d4b548bcd..edb8591c64 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# 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
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 835aa36a12..db1edcfe66 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# 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
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index b8301e2f27..755d29160c 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,7 +10,7 @@
- net5.0
+ net6.0
false
true
AllDisabledByDefault
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index b4a11ed5d6..7f8ece47dc 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
@@ -76,7 +77,7 @@ namespace DvdLib.Ifo
private void ReadVTS(ushort vtsNum, IReadOnlyList 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)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 68fc80c0a0..8fe9d484e7 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -366,7 +366,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(systemProfilesPath);
// 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);
}
@@ -486,18 +486,22 @@ namespace Emby.Dlna
}
///
- public ImageStream GetIcon(string filename)
+ public ImageStream? GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
+ var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream == null)
+ {
+ return null;
+ }
- return new ImageStream
+ return new ImageStream(stream)
{
- Format = format,
- Stream = _assembly.GetManifestResourceStream(resource)
+ Format = format
};
}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 970c16d2e6..1d4e3b047d 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,7 +17,7 @@
- net5.0
+ net6.0
false
true
AllDisabledByDefault
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 3c91360904..b39bd5ce9b 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
@@ -82,9 +83,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- header = header.Split('-')[^1];
-
- if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, _usCulture, out var val))
{
return val;
}
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index f14f73bb6f..4b92fbff43 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -45,10 +45,12 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
@@ -86,6 +88,7 @@ namespace Emby.Dlna.PlayTo
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
}
public async Task GetDataAsync(string url, CancellationToken cancellationToken)
@@ -94,12 +97,13 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 3f3dfccd3a..09525aae4e 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -250,8 +250,7 @@ namespace Emby.Dlna.Server
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) ?? string.Empty;
+ return SecurityElement.Escape(url);
}
private IEnumerable GetIcons()
diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs
index a97c4d63a6..68fd987585 100644
--- a/Emby.Dlna/Service/BaseService.cs
+++ b/Emby.Dlna/Service/BaseService.cs
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
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);
}
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index baf350c6f1..300eea9680 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
true
AllDisabledByDefault
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 0ad8bca314..9b130fdfd8 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -102,7 +102,7 @@ namespace Emby.Drawing
{
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);
}
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 07d879e96a..96f8f389b3 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
true
true
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 5edcf2f295..d200682e65 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
true
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 00b2f0f94c..bf6252c195 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,7 +19,7 @@
- net5.0
+ net6.0
false
true
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 3289e76098..381eb92a88 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
- DateTimeStyles.None).ToUniversalTime();
+ DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
var dateText = item.ToString();
- if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
+ if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
- result = dateTimeResult.ToUniversalTime();
+ result = dateTimeResult;
return true;
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 88fc5018df..0a48b844dd 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1150,7 +1150,7 @@ namespace Emby.Server.Implementations.Data
return null;
}
- if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
+ if (Enum.TryParse(imageType, true, out ImageType type))
{
image.Type = type;
}
@@ -1571,7 +1571,6 @@ namespace Emby.Server.Implementations.Data
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))
{
item.Audio = audio;
@@ -1610,18 +1609,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index++, out var lockedFields))
{
- IEnumerable GetLockedFields(string s)
+ List 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))
- {
- yield return parsedValue;
- }
+ (fields ??= new List()).Add(parsedValue);
}
}
- item.LockedFields = GetLockedFields(lockedFields).ToArray();
+ item.LockedFields = fields?.ToArray() ?? Array.Empty();
}
}
@@ -1647,18 +1644,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index, out var trailerTypes))
{
- IEnumerable GetTrailerTypes(string s)
+ List 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))
- {
- yield return parsedValue;
- }
+ (types ??= new List()).Add(parsedValue);
}
}
- trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
+ trailer.TrailerTypes = types?.ToArray() ?? Array.Empty();
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index ad4ad89d1f..0e1386ef55 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -23,16 +23,16 @@
-
+
-
+
-
+
@@ -42,7 +42,7 @@
- net5.0
+ net6.0
false
true
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 1bc229b0ca..77da46cd6d 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -246,9 +246,9 @@ namespace Emby.Server.Implementations.IO
{
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)
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index c7d1139639..bc5b4499fc 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// 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 UserRootFolder))
{
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// 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))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 6c65b58999..868071a992 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -4,6 +4,7 @@
using System;
using System.Globalization;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
@@ -41,6 +42,11 @@ namespace Emby.Server.Implementations.Library
return _closeFn();
}
+ public Stream GetStream()
+ {
+ throw new NotSupportedException();
+ }
+
public Task Open(CancellationToken openCancellationToken)
{
return Task.CompletedTask;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 8054beae3f..132486b4a6 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1250,10 +1250,8 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
- foreach (var file in files)
+ foreach (ReadOnlySpan file in files)
{
- // TODO: @bond use a ReadOnlySpan here when Enum.TryParse supports it
- // https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
@@ -2714,7 +2712,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List() : 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))
.ToList();
@@ -2758,7 +2756,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List() : 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))
.ToList();
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 16b45161fd..83acd8e9f2 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -10,9 +10,9 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 6f83973ba1..16231c73fc 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -587,13 +587,6 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public Task 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 OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{
var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
@@ -602,7 +595,8 @@ namespace Emby.Server.Implementations.Library
public async Task 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;
@@ -771,18 +765,19 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(true);
}
- public async Task> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
+ public Task> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
- return new Tuple(info.MediaSource, info as IDirectStreamProvider);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
+ return Task.FromResult(new Tuple(info.MediaSource, info as IDirectStreamProvider));
}
- private Task GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ public ILiveStream GetLiveStreamInfo(string id)
{
if (string.IsNullOrEmpty(id))
{
@@ -791,12 +786,16 @@ namespace Emby.Server.Implementations.Library
if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- return Task.FromResult(info);
- }
- else
- {
- return Task.FromException(new ResourceNotFoundException());
+ return info;
}
+
+ return null;
+ }
+
+ ///
+ public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
+ {
+ return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase));
}
public async Task GetLiveStream(string id, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index d6ae910565..cf2d22f4df 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent())
- && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
+ && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
{
var episode = ResolveVideo(args, false);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index c5a9a92ec2..41381d55bd 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
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)));
// 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();
- _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
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
-
- await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
+ var linkedCancellationToken = cancellationTokenSource.Token;
+
+ 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)
@@ -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)));
// 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();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 026b6bc0bb..64e54aa99e 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1990,7 +1990,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString(
"dateadded",
- DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
+ DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index d806a02959..3633fa3e10 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
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.
- _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 _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,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
- targetFile,
+ targetFile.Replace("\"", "\\\""), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 8202fab861..2e51ac8071 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using Jellyfin.XmlTv;
using Jellyfin.XmlTv.Entities;
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);
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);
}
@@ -89,11 +90,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return UnzipIfNeeded(path, cacheFile);
}
- private string UnzipIfNeeded(string originalUrl, string file)
+ private string UnzipIfNeeded(ReadOnlySpan originalUrl, string file)
{
- string ext = Path.GetExtension(originalUrl.Split('?')[0]);
+ ReadOnlySpan ext = Path.GetExtension(originalUrl.LeftPart('?'));
- if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 096b7f0454..2b82f24623 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -23,10 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
- protected readonly IServerConfigurationManager Config;
- protected readonly ILogger Logger;
- protected readonly IFileSystem FileSystem;
-
private readonly IMemoryCache _memoryCache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IFileSystem fileSystem, IMemoryCache memoryCache)
@@ -37,12 +33,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileSystem = fileSystem;
}
- public virtual bool IsSupported => true;
+ protected IServerConfigurationManager Config { get; }
- protected abstract Task> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+ protected ILogger Logger { get; }
+
+ protected IFileSystem FileSystem { get; }
+
+ public virtual bool IsSupported => true;
public abstract string Type { get; }
+ protected virtual string ChannelIdPrefix => Type + "_";
+
+ protected abstract Task> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public async Task> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
var key = tuner.Id;
@@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException();
}
- protected virtual string ChannelIdPrefix => Type + "_";
-
protected virtual bool IsValidChannelId(string channelId)
{
if (string.IsNullOrEmpty(channelId))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 2bd12a9c8f..4d538c6043 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -36,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
- private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions;
@@ -50,7 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- INetworkManager networkManager,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
@@ -58,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
- _networkManager = networkManager;
_streamHelper = streamHelper;
_jsonOptions = JsonDefaults.Options;
@@ -70,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override string ChannelIdPrefix => "hdhr_";
- private string GetChannelId(TunerHostInfo info, Channels i)
+ private string GetChannelId(Channels i)
=> ChannelIdPrefix + i.GuideNumber;
internal async Task> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
@@ -103,7 +100,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
Name = i.GuideName,
Number = i.GuideNumber,
- Id = GetChannelId(tuner, i),
+ Id = GetChannelId(i),
IsFavorite = i.Favorite,
TunerHostId = tuner.Id,
IsHD = i.HD,
@@ -255,7 +252,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- var tuners = new List();
+ var tuners = new List(model.TunerCount);
var uri = new Uri(GetApiUrl(info));
@@ -264,10 +261,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Legacy HdHomeruns are IPv4 only
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 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 status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
tuners.Add(new LiveTvTunerInfo
@@ -455,28 +452,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Path = url,
Protocol = MediaProtocol.Udp,
MediaStreams = new List
- {
- new MediaStream
- {
- Type = MediaStreamType.Video,
- // Set the index to -1 because we don't know the exact index of the video stream within the container
- Index = -1,
- IsInterlaced = isInterlaced,
- Codec = videoCodec,
- Width = width,
- Height = height,
- BitRate = videoBitrate,
- NalLengthSize = nal
- },
- new MediaStream
- {
- Type = MediaStreamType.Audio,
- // Set the index to -1 because we don't know the exact index of the audio stream within the container
- Index = -1,
- Codec = audioCodec,
- BitRate = audioBitrate
- }
- },
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = isInterlaced,
+ Codec = videoCodec,
+ Width = width,
+ Height = height,
+ BitRate = videoBitrate,
+ NalLengthSize = nal
+ },
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ // Set the index to -1 because we don't know the exact index of the audio stream within the container
+ Index = -1,
+ Codec = audioCodec,
+ BitRate = audioBitrate
+ }
+ },
RequiresOpening = true,
RequiresClosing = true,
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);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 58e0c7448a..31445e1ecc 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -101,7 +101,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- if (localAddress.IsIPv4MappedToIPv6) {
+ if (localAddress.IsIPv4MappedToIPv6)
+ {
localAddress = localAddress.MapToIPv4();
}
@@ -156,11 +157,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
- public string GetFilePath()
- {
- return TempFilePath;
- }
-
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken)
{
using (udpClient)
@@ -184,7 +180,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = false;
}
- await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false);
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
}
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken)
@@ -201,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
cancellationToken,
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)
{
resTask.Dispose();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 2c21a4a893..5581ba87c5 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -3,10 +3,8 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -22,14 +20,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
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(
MediaSourceInfo mediaSource,
TunerHostInfo tuner,
@@ -57,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
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; }
@@ -97,121 +95,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
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(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
+ FileOptions.SequentialScan | FileOptions.Asynchronous);
- public Task DeleteTempFiles()
- {
- return DeleteTempFiles(GetStreamFilePaths());
- }
-
- protected async Task DeleteTempFiles(IEnumerable paths, int retryCount = 0)
+ protected async Task DeleteTempFiles(string path, int retryCount = 0)
{
if (retryCount == 0)
{
- Logger.LogInformation("Deleting temp files {0}", paths);
- }
-
- var failedFiles = new List();
-
- foreach (var path in paths)
- {
- if (!File.Exists(path))
- {
- continue;
- }
-
- 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);
+ Logger.LogInformation("Deleting temp file {FilePath}", path);
}
- }
-
- protected virtual List GetStreamFilePaths()
- {
- return new List { 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))
+ try
{
- return (files[^1], true);
+ FileSystem.DeleteFile(path);
}
-
- 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))
+ catch (Exception ex)
{
- if (seekFile)
+ Logger.LogError(ex, "Error deleting file {FilePath}", path);
+ if (retryCount <= 40)
{
- TrySeek(inputStream, -20000);
+ await Task.Delay(500).ConfigureAwait(false);
+ await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
}
-
- await StreamHelper.CopyToAsync(
- inputStream,
- stream,
- IODefaults.CopyToBufferSize,
- emptyReadLimit,
- cancellationToken).ConfigureAwait(false);
}
}
- private void TrySeek(FileStream stream, long offset)
+ private void TrySeek(Stream stream, long offset)
{
if (!stream.CanSeek)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 23071a4306..506ef5548a 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
if (!IsValidChannelNumber(numberString))
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 8629938777..3b69e55b01 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
@@ -55,39 +54,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
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.
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
- var extension = "ts";
- var requiresRemux = false;
-
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
- if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
- else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
- 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
- if (requiresRemux)
+ 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))
{
- using (response)
- {
- return;
- }
+ // Close the stream without any sharing features
+ response.Dispose();
+ return;
}
- SetTempFilePath(extension);
+ SetTempFilePath("ts");
var taskCompletionSource = new TaskCompletionSource();
@@ -117,16 +103,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
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));
}
}
- public string GetFilePath()
- {
- return TempFilePath;
- }
-
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(
@@ -134,10 +115,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
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;
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(
stream,
fileStream,
@@ -147,19 +128,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
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);
}
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.TrySetResult(false);
EnableStreamSharing = false;
- await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false);
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
},
CancellationToken.None);
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index be629c8a42..5a4a4d5a91 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,5 +1,5 @@
{
- "Albums": "البومات",
+ "Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "الفنانين",
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
- "Collections": "مجموعات",
+ "Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3c51d64e04..2a56d07450 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 0e4c38425c..c3e52eb81a 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Rafraîchir les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"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.",
- "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
+ "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshLibrary": "Scanner la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index afb22ab472..b433c6f68c 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -48,7 +48,7 @@
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteAlbums": "Álbunes Favoritos",
"HeaderContinueWatching": "Seguir mirando",
- "HeaderAlbumArtists": "Artistas de Album",
+ "HeaderAlbumArtists": "Artistas do Album",
"Genres": "Xéneros",
"Forced": "Forzado",
"Folders": "Cartafoles",
@@ -117,5 +117,7 @@
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
"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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 9eb80b83ba..d7cda61da6 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
- "HeaderAlbumArtists": "Izvođači na albumu",
+ "HeaderAlbumArtists": "Album od izvođača",
"HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index e36fdc43db..066ef55878 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -74,7 +74,7 @@
"NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi",
- "MusicVideos": "Videot muzikore",
+ "MusicVideos": "Video muzikore",
"Music": "Muzikë",
"Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier",
@@ -96,7 +96,7 @@
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
- "HeaderAlbumArtists": "Artistët e albumeve",
+ "HeaderAlbumArtists": "Artistët e Albumeve",
"Genres": "Zhanret",
"Folders": "Skedarët",
"Favorites": "Të preferuarat",
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 5d6d0775c4..11af9fc987 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -90,7 +90,7 @@
"NameSeasonUnknown": "نامعلوم باب",
"NameSeasonNumber": "باب {0}",
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
- "MusicVideos": "موسیقی ویڈیو",
+ "MusicVideos": "ویڈیو موسیقی",
"Music": "موسیقی",
"MixedContent": "مخلوط مواد",
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
@@ -99,18 +99,19 @@
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
"Latest": "تازہ ترین",
"LabelRunningTimeValue": "چلانے کی مدت",
- "LabelIpAddressValue": "ای پی پتے {0}",
+ "LabelIpAddressValue": "آئ پی ایڈریس {0}",
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
- "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
+ "FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
"ChapterNameValue": "باب",
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
"Application": "پروگرام",
- "AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
+ "AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
+ "Forced": "جَبری"
}
diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
index 4160f3a500..8b1cee89d1 100644
--- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
@@ -17,6 +17,15 @@ namespace Emby.Server.Implementations.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)
{
return base.IsVisible(user) && GetChildren(user, true).Any();
@@ -27,15 +36,6 @@ namespace Emby.Server.Implementations.Playlists
return base.GetEligibleChildrenForRecursiveChildren(user).OfType();
}
- [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 GetItemsInternal(InternalItemsQuery query)
{
if (query.User == null)
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index b8e1dc2c05..d52c0b2a14 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -8,10 +8,10 @@ using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
@@ -39,14 +39,6 @@ namespace Emby.Server.Implementations.Plugins
private IHttpClientFactory? _httpClientFactory;
- private IHttpClientFactory HttpClientFactory
- {
- get
- {
- return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve());
- }
- }
-
///
/// Initializes a new instance of the class.
///
@@ -86,6 +78,14 @@ namespace Emby.Server.Implementations.Plugins
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List();
}
+ private IHttpClientFactory HttpClientFactory
+ {
+ get
+ {
+ return _httpClientFactory ??= _appHost.Resolve();
+ }
+ }
+
///
/// Gets the Plugins.
///
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index ae773c6589..c81c269945 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.QuickConnect
///
/// Quick connect implementation.
///
- public class QuickConnectManager : IQuickConnect, IDisposable
+ public class QuickConnectManager : IQuickConnect
{
///
/// The length of user facing codes.
@@ -30,7 +30,6 @@ namespace Emby.Server.Implementations.QuickConnect
///
private const int Timeout = 10;
- private readonly RNGCryptoServiceProvider _rng = new ();
private readonly ConcurrentDictionary _currentRequests = new ();
private readonly ConcurrentDictionary _authorizedSecrets = new ();
@@ -140,7 +139,7 @@ namespace Emby.Server.Implementations.QuickConnect
uint scale = uint.MaxValue;
while (scale == uint.MaxValue)
{
- _rng.GetBytes(raw);
+ RandomNumberGenerator.Fill(raw);
scale = BitConverter.ToUInt32(raw);
}
@@ -199,31 +198,10 @@ namespace Emby.Server.Implementations.QuickConnect
return result.AuthenticationResult;
}
- ///
- /// Dispose.
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Dispose.
- ///
- /// Dispose unmanaged resources.
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- _rng.Dispose();
- }
- }
-
private string GenerateSecureRandom(int length = 32)
{
Span bytes = stackalloc byte[length];
- _rng.GetBytes(bytes);
+ RandomNumberGenerator.Fill(bytes);
return Convert.ToHexString(bytes);
}
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 2b0ab536f9..037eb170ac 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -28,16 +28,6 @@ namespace Emby.Server.Implementations.Sorting
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 episode2 = y as Episode;
@@ -156,8 +146,14 @@ namespace Emby.Server.Implementations.Sorting
{
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.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;
}
///
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
index 9815e252ee..dd0bd4ec2f 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
@@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
}
///
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
- context.Succeed(firstTimeSetupOrDefaultRequirement);
+ context.Succeed(requirement);
return Task.CompletedTask;
}
var validated = ValidateClaims(context.User);
if (validated)
{
- context.Succeed(firstTimeSetupOrDefaultRequirement);
+ context.Succeed(requirement);
}
else
{
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index decbe0c035..90b76ee99a 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -33,18 +33,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
}
///
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement firstTimeSetupOrElevatedRequirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
- context.Succeed(firstTimeSetupOrElevatedRequirement);
+ context.Succeed(requirement);
return Task.CompletedTask;
}
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
- context.Succeed(firstTimeSetupOrElevatedRequirement);
+ context.Succeed(requirement);
}
else
{
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 9dc280e138..b1c860d61f 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// 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);
if (user.ProfileImage != null)
{
@@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// 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);
if (user.ProfileImage != null)
{
@@ -341,7 +341,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// 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 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);
// 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 item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -2026,7 +2026,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- return PhysicalFile(imagePath, imageContentType);
+ return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 93dc767291..b131530c93 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1199,15 +1199,15 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
- public async Task 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)
{
return NotFound();
}
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 7a2c23991b..8a33b12f4c 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -7,6 +7,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -199,13 +200,13 @@ namespace Jellyfin.Api.Controllers
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 fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
// 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);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index e6584f0fe6..741bdfee97 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
// For older files, assume fully static
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");
}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index 7df51c7afb..e7c5a71257 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -21,10 +21,10 @@ namespace Jellyfin.Api.Controllers
public ActionResult GetUtcTime()
{
// 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
- var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
+ var responseTransmissionTime = DateTime.UtcNow;
// Implementing NTP on such a high level results in this useless
// information being sent. On the other hand it enables future additions.
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 7c5b8a43b7..6eada67cf0 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -147,7 +147,7 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
+ var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
var parentIdGuid = parentId ?? Guid.Empty;
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index bc6fc904a1..150f22d1b8 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -453,14 +453,15 @@ namespace Jellyfin.Api.Controllers
{
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
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
+ return NotFound();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// 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
@@ -492,13 +493,8 @@ namespace Jellyfin.Api.Controllers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return File(Response.Body, contentType);
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return File(liveStream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index ddcde1cf6d..a5e47b8eca 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,4 +1,5 @@
-using System.Net.Http;
+using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
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);
- await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo == null)
+ {
+ throw new FileNotFoundException();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// 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
@@ -159,13 +161,8 @@ namespace Jellyfin.Api.Helpers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
+ var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index b0fd59e5e3..6385b62c96 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
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.
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";
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index f36769dc2a..4567621470 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -38,7 +38,7 @@ namespace Jellyfin.Api.Helpers
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan);
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
using var reader = new StreamReader(fileStream);
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
deleted file mode 100644
index 81970b041a..0000000000
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Progressive file copier.
- ///
- 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;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The path to copy from.
- /// The transcoding job.
- /// Instance of the .
- /// The cancellation token.
- public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
- {
- _path = path;
- _job = job;
- _cancellationToken = cancellationToken;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// The transcoding job.
- /// Instance of the .
- /// The cancellation token.
- public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
- {
- _directStreamProvider = directStreamProvider;
- _job = job;
- _cancellationToken = cancellationToken;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- ///
- /// Gets or sets a value indicating whether allow read end of file.
- ///
- public bool AllowEndOfFile { get; set; } = true;
-
- ///
- /// Gets or sets copy start position.
- ///
- public long StartPosition { get; set; }
-
- ///
- /// Write source stream to output.
- ///
- /// Output stream.
- /// Cancellation token.
- /// A .
- 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 CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
- {
- var array = ArrayPool.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.Shared.Return(array);
- }
- }
- }
-}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index d4cc0172d9..61e18220a1 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -13,11 +13,10 @@ namespace Jellyfin.Api.Helpers
///
public class ProgressiveFileStream : Stream
{
- private readonly FileStream _fileStream;
+ private readonly Stream _stream;
private readonly TranscodingJobDto? _job;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly TranscodingJobHelper? _transcodingJobHelper;
private readonly int _timeoutMs;
- private readonly bool _allowAsyncFileRead;
private int _bytesWritten;
private bool _disposed;
@@ -33,23 +32,25 @@ namespace Jellyfin.Api.Helpers
_job = job;
_transcodingJobHelper = transcodingJobHelper;
_timeoutMs = timeoutMs;
- _bytesWritten = 0;
- var fileOptions = FileOptions.SequentialScan;
- _allowAsyncFileRead = false;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (AsyncFile.UseAsyncIO)
- {
- fileOptions |= FileOptions.Asynchronous;
- _allowAsyncFileRead = true;
- }
+ _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
+ }
- _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The stream to progressively copy.
+ /// The timeout duration in milliseconds.
+ public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
+ {
+ _job = null;
+ _transcodingJobHelper = null;
+ _timeoutMs = timeoutMs;
+ _stream = stream;
}
///
- public override bool CanRead => _fileStream.CanRead;
+ public override bool CanRead => _stream.CanRead;
///
public override bool CanSeek => false;
@@ -70,13 +71,13 @@ namespace Jellyfin.Api.Helpers
///
public override void Flush()
{
- _fileStream.Flush();
+ _stream.Flush();
}
///
public override int Read(byte[] buffer, int offset, int count)
{
- return _fileStream.Read(buffer, offset, count);
+ return _stream.Read(buffer, offset, count);
}
///
@@ -90,15 +91,7 @@ namespace Jellyfin.Api.Helpers
while (remainingBytesToRead > 0)
{
cancellationToken.ThrowIfCancellationRequested();
- int bytesRead;
- if (_allowAsyncFileRead)
- {
- bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
- }
+ int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
remainingBytesToRead -= bytesRead;
newOffset += bytesRead;
@@ -152,11 +145,11 @@ namespace Jellyfin.Api.Helpers
{
if (disposing)
{
- _fileStream.Dispose();
+ _stream.Dispose();
if (_job != null)
{
- _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ _transcodingJobHelper?.OnTranscodeEndRequest(_job);
}
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 0041251e3d..4fc791665e 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@@ -81,7 +82,7 @@ namespace Jellyfin.Api.Helpers
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))
{
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 4e1e98df0d..14f287aef6 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -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");
// 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);
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 2fca88f24e..8cc4711a72 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
true
AD0001
@@ -14,7 +14,7 @@
-
+
diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
index be2045fbab..d2e78ac884 100644
--- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
@@ -32,7 +32,8 @@ namespace Jellyfin.Api.ModelBinders
{
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);
}
catch (FormatException e)
@@ -44,4 +45,4 @@ namespace Jellyfin.Api.ModelBinders
return Task.CompletedTask;
}
}
-}
\ No newline at end of file
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index e95f2d1f43..0f84faeafc 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -60,6 +60,9 @@ namespace Jellyfin.Api.Models.StreamingDtos
///
/// Gets or sets the direct stream provicer.
///
+ ///
+ /// Deprecated.
+ ///
public IDirectStreamProvider? DirectStreamProvider { get; set; }
///
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 65bbd49da2..19aef704c0 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
true
true
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 8cee5dcaee..5fa386ecac 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
true
@@ -28,11 +28,6 @@
-
-
-
-
-
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 227a41ce44..0cd9a59156 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -1,6 +1,6 @@
- net5.0
+ net6.0
false
true
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index a75b285936..337f5cb82a 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
true
@@ -19,13 +19,13 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
index 0d04b6bb13..b061be33b5 100644
--- a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
+++ b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Configuration
}
///
- public Task GetPolicyAsync(HttpContext context, string policyName)
+ public Task GetPolicyAsync(HttpContext context, string? policyName)
{
var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
var builder = new CorsPolicyBuilder()
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Configuration
.AllowCredentials();
}
- return Task.FromResult(builder.Build());
+ return Task.FromResult(builder.Build());
}
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index f19e87aba5..183480487a 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -278,7 +278,7 @@ namespace Jellyfin.Server.Extensions
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
- Name = "X-Emby-Authorization",
+ Name = "Authorization",
Description = "API key header parameter"
});
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
new file mode 100644
index 0000000000..be4926da63
--- /dev/null
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -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
+{
+ ///
+ public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An instance of the interface.
+ public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
+ {
+ }
+
+ ///
+ 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
+ };
+ }
+
+ ///
+ 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;
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 1fdad73b74..074d43fba8 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -8,7 +8,7 @@
jellyfin
Exe
- net5.0
+ net6.0
false
false
true
@@ -33,8 +33,8 @@
-
-
+
+
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
index fd0ebbf438..cdd86e28e6 100644
--- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -27,7 +27,11 @@ namespace Jellyfin.Server.Middleware
/// The async task.
public async Task Invoke(HttpContext httpContext)
{
- httpContext.Features.Set(new UrlDecodeQueryFeature(httpContext.Features.Get()));
+ var feature = httpContext.Features.Get();
+ if (feature != null)
+ {
+ httpContext.Features.Set(new UrlDecodeQueryFeature(feature));
+ }
await _next(httpContext).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
index c1f5b5dfaf..b5f515cdab 100644
--- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -52,20 +52,14 @@ namespace Jellyfin.Server.Middleware
return;
}
- // Unencode and re-parse querystring.
- var unencodedKey = HttpUtility.UrlDecode(key);
-
- if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ if (!key.Contains('='))
{
- // Don't do anything if it's not encoded.
_store = value;
return;
}
var pairs = new Dictionary();
- var queryString = unencodedKey.SpanSplit('&');
-
- foreach (var pair in queryString)
+ foreach (var pair in key.SpanSplit('&'))
{
var i = pair.IndexOf('=');
if (i == -1)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index d9524645a1..9b2d603c7f 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -1,7 +1,6 @@
using System;
using System.IO;
using Emby.Server.Implementations.Data;
-using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@@ -10,6 +9,7 @@ using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger _logger;
private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider;
- private readonly MyXmlSerializer _xmlSerializer;
+ private readonly IXmlSerializer _xmlSerializer;
///
/// Initializes a new instance of the class.
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
ILogger logger,
IServerApplicationPaths paths,
JellyfinDbProvider provider,
- MyXmlSerializer xmlSerializer)
+ IXmlSerializer xmlSerializer)
{
_logger = logger;
_paths = paths;
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3c0ee069d4..f36675b955 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -195,9 +195,9 @@ namespace Jellyfin.Server
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.");
throw;
@@ -547,7 +547,7 @@ namespace Jellyfin.Server
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
// 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);
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 60cdc2f6fe..8085c26308 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -7,6 +7,7 @@ using System.Text;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Infrastructure;
using Jellyfin.Server.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -14,6 +15,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -56,6 +59,9 @@ namespace Jellyfin.Server
{
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
+
+ // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
+ services.AddSingleton, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinApiSwagger();
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 12cfaf9789..6358c00001 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -29,7 +29,7 @@
- net5.0
+ net6.0
false
true
true
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
index a64919700d..cc0a107a81 100644
--- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs
+++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
@@ -74,6 +74,6 @@ namespace MediaBrowser.Controller.Dlna
///
/// The filename.
/// DlnaIconResponse.
- ImageStream GetIcon(string filename);
+ ImageStream? GetIcon(string filename);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index c7f61a90bb..7ca0e851bd 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Drawing
/// Guid.
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
- string GetImageCacheTag(BaseItem item, ChapterInfo info);
+ string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
index 5d552170f9..f4c3057993 100644
--- a/MediaBrowser.Controller/Drawing/ImageStream.cs
+++ b/MediaBrowser.Controller/Drawing/ImageStream.cs
@@ -8,11 +8,16 @@ namespace MediaBrowser.Controller.Drawing
{
public class ImageStream : IDisposable
{
+ public ImageStream(Stream stream)
+ {
+ Stream = stream;
+ }
+
///
- /// Gets or sets the stream.
+ /// Gets the stream.
///
/// The stream.
- public Stream? Stream { get; set; }
+ public Stream Stream { get; }
///
/// Gets or sets the format.
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index f4c91973bd..1237268d7c 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -44,18 +44,10 @@ namespace MediaBrowser.Controller.Entities
///
/// The trailer folder name.
///
- public const string TrailerFolderName = "trailers";
+ public const string TrailersFolderName = "trailers";
public const string ThemeSongsFolderName = "theme-music";
- public const string ThemeSongFilename = "theme";
+ public const string ThemeSongFileName = "theme";
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";
///
/// The supported image extensions.
@@ -93,16 +85,20 @@ namespace MediaBrowser.Controller.Entities
};
public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
- public static readonly string[] AllExtrasTypesFolderNames =
- {
- ExtrasFolderName,
- BehindTheScenesFolderName,
- DeletedScenesFolderName,
- InterviewFolderName,
- SceneFolderName,
- SampleFolderName,
- ShortsFolderName,
- FeaturettesFolderName
+
+ ///
+ /// The supported extra folder names and types. See .
+ ///
+ public static readonly Dictionary AllExtrasTypesFolderNames = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown,
+ ["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes,
+ ["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene,
+ ["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview,
+ ["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene,
+ ["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample,
+ ["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip,
+ ["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip
};
private string _sortName;
@@ -1358,7 +1354,7 @@ namespace MediaBrowser.Controller.Entities
// Support plex/xbmc convention
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())
.OfType()
@@ -1417,39 +1413,24 @@ namespace MediaBrowser.Controller.Entities
protected virtual BaseItem[] LoadExtras(List fileSystemChildren, IDirectoryService directoryService)
{
- var extras = new List
- net5.0
+ net6.0
false
true
true
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index b09b7dba6c..e92c4a08a6 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -541,7 +541,12 @@ namespace MediaBrowser.Controller.MediaEncoding
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)
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index aa5e2c4038..c4ddc5618d 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var size = part.Split('=', 2)[^1];
int? scale = null;
- if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
+ if (size.Contains("kb", StringComparison.OrdinalIgnoreCase))
{
scale = 1024;
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -139,7 +139,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var rate = part.Split('=', 2)[^1];
int? scale = null;
- if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
+ if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase))
{
scale = 1024;
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 1cf8fcd1b5..a3db717b9f 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -11,7 +11,7 @@
- net5.0
+ net6.0
false
true
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index ef130ee747..7c9e681d6a 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -144,9 +145,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
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
{
@@ -331,7 +332,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
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;
}
@@ -534,9 +535,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
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;
}
}
@@ -551,9 +552,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
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;
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index a7bcaf5445..4cbd1bbc80 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -165,14 +165,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
// User had cleared the custom path in UI
newPath = string.Empty;
}
- else if (Directory.Exists(path))
- {
- // Given path is directory, so resolve down to filename
- newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
- }
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
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 6da9886a4b..30cfb904e1 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
true
@@ -23,7 +23,7 @@
-
+
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 9196fe1397..a9e753726d 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -63,10 +63,10 @@ namespace MediaBrowser.MediaEncoding.Probing
public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary tags, string key)
{
if (tags.TryGetValue(key, out var val)
- && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var dateTime)
- || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out dateTime)))
+ && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)
+ || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out dateTime)))
{
- return dateTime.ToUniversalTime();
+ return dateTime;
}
return null;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 2516aad1cc..26f629a315 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1378,7 +1378,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var disc = tags.GetValueOrDefault(tagName);
- if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.Split('/')[0], out var discNum))
+ if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
{
return discNum;
}
@@ -1450,9 +1450,9 @@ namespace MediaBrowser.MediaEncoding.Probing
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
- if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.None, out var parsedDate))
+ if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.AdjustToUniversal, out var parsedDate))
{
- video.PremiereDate = parsedDate.ToUniversalTime();
+ video.PremiereDate = parsedDate;
}
var description = tags.GetValueOrDefault("WM/SubTitleDescription");
@@ -1468,7 +1468,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (string.IsNullOrWhiteSpace(subTitle)
&& !string.IsNullOrWhiteSpace(description)
- && description.AsSpan()[0..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+ && description.AsSpan()[..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].Contains(':')) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] descriptionParts = description.Split(':');
if (descriptionParts.Length > 0)
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 24ceb1b571..52c1b64677 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -5,7 +5,7 @@ using System.Threading;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core;
+using Nikse.SubtitleEdit.Core.Common;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
@@ -38,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
subRip.LoadSubtitle(subtitle, lines, "untitled");
if (subRip.ErrorCount > 0)
{
- _logger.LogError("{ErrorCount} errors encountered while parsing subtitle.");
+ _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount);
}
var trackInfo = new SubtitleTrackInfo();
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 6f6178af24..022cdbe9d3 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -195,7 +195,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return AsyncFile.OpenRead(fileInfo.Path);
}
- private async Task GetReadableFile(
+ internal async Task GetReadableFile(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -205,9 +205,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string outputFormat;
string outputCodec;
- if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
{
// Extract
outputCodec = "copy";
@@ -238,7 +238,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
- if (TryGetReader(currentFormat, out _))
+ if (!TryGetReader(currentFormat, out _))
{
// Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
@@ -248,12 +248,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
}
- if (subtitleStream.IsExternal)
- {
- return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
- }
-
- return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
+ // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
+ return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
}
private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
@@ -684,7 +680,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal))
{
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
using (var writer = new StreamWriter(fileStream, encoding))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
@@ -756,7 +752,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- private struct SubtitleInfo
+ internal readonly struct SubtitleInfo
{
public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
{
@@ -766,13 +762,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IsExternal = isExternal;
}
- public string Path { get; set; }
+ public string Path { get; }
- public MediaProtocol Protocol { get; set; }
+ public MediaProtocol Protocol { get; }
- public string Format { get; set; }
+ public string Format { get; }
- public bool IsExternal { get; set; }
+ public bool IsExternal { get; }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index ad32cb7943..6d56dda91f 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -18,14 +18,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
writer.WriteLine("REGION");
writer.WriteLine("id:subtitle");
writer.WriteLine("width:80%");
writer.WriteLine("lines:3");
writer.WriteLine("regionanchor:50%,100%");
writer.WriteLine("viewportanchor:50%,90%");
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs
index b888a41639..f38ed9ae3d 100644
--- a/MediaBrowser.Model/IO/AsyncFile.cs
+++ b/MediaBrowser.Model/IO/AsyncFile.cs
@@ -8,20 +8,13 @@ namespace MediaBrowser.Model.IO
///
public static class AsyncFile
{
- ///
- /// Gets a value indicating whether we should use async IO on this platform.
- /// .
- ///
- /// Returns false on Windows; otherwise true.
- public static bool UseAsyncIO => !OperatingSystem.IsWindows();
-
///
/// Opens an existing file for reading.
///
/// The file to be opened for reading.
/// A read-only on the specified path.
public static FileStream OpenRead(string path)
- => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, UseAsyncIO);
+ => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
///
/// Opens an existing file for writing.
@@ -29,6 +22,6 @@ namespace MediaBrowser.Model.IO
/// The file to be opened for writing.
/// An unshared object on the specified path with Write access.
public static FileStream OpenWrite(string path)
- => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, UseAsyncIO);
+ => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index a371afc2cf..b0a12a9c90 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -14,7 +14,7 @@
- net5.0
+ net6.0
false
true
true
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 96f5ab51ae..748170a0e1 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -2,8 +2,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Net
{
@@ -163,15 +165,16 @@ namespace MediaBrowser.Model.Net
return dict;
}
- public static string? GetMimeType(string path) => GetMimeType(path, true);
+ public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
///
/// Gets the type of the MIME.
///
/// The filename to find the MIME type of.
- /// Whether of not to return a default value if no fitting MIME type is found.
- /// The worrect MIME type for the given filename, or `null` if it wasn't found and is false.
- public static string? GetMimeType(string filename, bool enableStreamDefault)
+ /// The default value to return if no fitting MIME type is found.
+ /// The correct MIME type for the given filename, or if it wasn't found.
+ [return: NotNullIfNotNullAttribute("defaultValue")]
+ public static string? GetMimeType(string filename, string? defaultValue = null)
{
if (filename.Length == 0)
{
@@ -210,7 +213,7 @@ namespace MediaBrowser.Model.Net
return "application/octet-stream";
}
- return enableStreamDefault ? "application/octet-stream" : null;
+ return defaultValue;
}
public static string? ToExtension(string mimeType)
@@ -221,7 +224,7 @@ namespace MediaBrowser.Model.Net
}
// handle text/html; charset=UTF-8
- mimeType = mimeType.Split(';')[0];
+ mimeType = mimeType.AsSpan().LeftPart(';').ToString();
if (_extensionLookup.TryGetValue(mimeType, out string? result))
{
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 6c14c8de1e..7d259a9d36 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -264,7 +264,7 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 7fdef6b449..73bca5aa59 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Manager
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
@@ -486,7 +486,20 @@ namespace MediaBrowser.Providers.Manager
try
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
+
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
@@ -500,15 +513,8 @@ namespace MediaBrowser.Providers.Manager
result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad url's. Just move to the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
@@ -588,6 +594,19 @@ namespace MediaBrowser.Providers.Manager
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
// If there's already an image of the same size, skip it
if (response.Content.Headers.ContentLength.HasValue)
{
@@ -615,15 +634,8 @@ namespace MediaBrowser.Providers.Manager
cancellationToken).ConfigureAwait(false);
result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad urls. Just move onto the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 52611c427e..29d6b01f23 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -26,7 +26,7 @@
- net5.0
+ net6.0
false
true
../jellyfin.ruleset
@@ -47,5 +47,7 @@
+
+
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index c1226febfc..3e0b0014eb 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 8572b3413d..e0b2f9c588 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -155,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
index 268538815e..19d90b9a1b 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (reader.TokenType == JsonTokenType.String)
{
var str = reader.GetString();
- if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ if (str == null || str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 1dea3dece1..479ae0f391 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -295,7 +295,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam));
var rootObject = await GetDeserializedOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
@@ -335,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
seasonId));
var rootObject = await GetDeserializedOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
new file mode 100644
index 0000000000..907f0160d6
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ ///
+ /// Plugin configuration class for TMDb library.
+ ///
+ public class PluginConfiguration : BasePluginConfiguration
+ {
+ ///
+ /// Gets or sets a value indicating whether include adult content when searching with TMDb.
+ ///
+ public bool IncludeAdult { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
new file mode 100644
index 0000000000..6f42549d7d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -0,0 +1,52 @@
+
+
+
+ TMDb
+
+
+
+
+
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
new file mode 100644
index 0000000000..ea81eb96e0
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ ///
+ /// Plugin class for the TMDb library.
+ ///
+ public class Plugin : BasePlugin, IHasWebPages
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// application paths.
+ /// xml serializer.
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ ///
+ /// Gets the instance of TMDb plugin.
+ ///
+ public static Plugin Instance { get; private set; }
+
+ ///
+ public override Guid Id => new Guid("b8715ed1-6c47-4528-9ad3-f72deb539cd4");
+
+ ///
+ public override string Name => "TMDb";
+
+ ///
+ public override string Description => "Get metadata for movies and other video content from TheMovieDb.";
+
+ // TODO remove when plugin removed from server.
+
+ ///
+ public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml";
+
+ ///
+ /// Return the plugin configuration page.
+ ///
+ /// PluginPageInfo.
+ public IEnumerable GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 4de4bf4db6..5bd5dd2e80 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -358,7 +358,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -386,7 +386,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchPersonAsync(name, cancellationToken: cancellationToken)
+ .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -428,7 +428,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
+ .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 7a057c065f..7b2454efc9 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Studios
Directory.CreateDirectory(Path.GetDirectoryName(file));
await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index d6c346ba12..772e617abb 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Subtitles
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, FileOptions.Asynchronous);
await stream.CopyToAsync(fs).ConfigureAwait(false);
return;
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 3e2a9bacf1..926be5a927 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,7 +15,7 @@
- net5.0
+ net6.0
false
true
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index f975278fbc..f7f4ea0652 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
@@ -267,9 +268,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- item.DateCreated = added.ToUniversalTime();
+ item.DateCreated = added;
}
else
{
@@ -383,9 +384,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && userData != null)
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- userData.LastPlayedDate = added.ToUniversalTime();
+ userData.LastPlayedDate = added;
}
else
{
@@ -474,7 +475,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
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;
}
@@ -684,9 +685,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.PremiereDate = date.ToUniversalTime();
+ item.PremiereDate = date;
item.ProductionYear = date.Year;
}
}
@@ -702,9 +703,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.EndDate = date.ToUniversalTime();
+ item.EndDate = date;
}
}
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
index 7d6a471f95..5d7da4124e 100644
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ b/RSSDP/DisposableManagedObjectBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Text;
namespace Rssdp.Infrastructure
@@ -45,11 +46,11 @@ namespace Rssdp.Infrastructure
const string ArgFormat = "{0}: {1}\r\n";
- builder.AppendFormat("{0}\r\n", header);
+ builder.AppendFormat(CultureInfo.InvariantCulture, "{0}\r\n", header);
foreach (var pair in values)
{
- builder.AppendFormat(ArgFormat, pair.Key, pair.Value);
+ builder.AppendFormat(CultureInfo.InvariantCulture, ArgFormat, pair.Key, pair.Value);
}
builder.Append("\r\n");
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index 54113d4644..77130983b5 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -11,7 +11,7 @@
- net5.0
+ net6.0
false
AllDisabledByDefault
disable
diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs
index 4005d836d9..c826830f1d 100644
--- a/RSSDP/SsdpDevice.cs
+++ b/RSSDP/SsdpDevice.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Globalization;
using Rssdp.Infrastructure;
namespace Rssdp
@@ -134,11 +135,13 @@ namespace Rssdp
{
get
{
- return String.Format("urn:{0}:{3}:{1}:{2}",
- this.DeviceTypeNamespace ?? String.Empty,
- this.DeviceType ?? String.Empty,
- this.DeviceVersion,
- this.DeviceClass ?? "device");
+ return String.Format(
+ CultureInfo.InvariantCulture,
+ "urn:{0}:{3}:{1}:{2}",
+ this.DeviceTypeNamespace ?? String.Empty,
+ this.DeviceType ?? String.Empty,
+ this.DeviceVersion,
+ this.DeviceClass ?? "device");
}
}
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index c9e795d565..64d19803df 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
@@ -233,7 +234,7 @@ namespace Rssdp.Infrastructure
{
if (String.IsNullOrEmpty(searchTarget))
{
- WriteTrace(String.Format("Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
+ WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
return;
}
@@ -340,7 +341,7 @@ namespace Rssdp.Infrastructure
private string GetUsn(string udn, string fullDeviceType)
{
- return String.Format("{0}::{1}", udn, fullDeviceType);
+ return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
}
private async void SendSearchResponse(
@@ -363,7 +364,7 @@ namespace Rssdp.Infrastructure
values["DATE"] = DateTime.UtcNow.ToString("r");
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
values["ST"] = searchTarget;
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["USN"] = uniqueServiceName;
values["LOCATION"] = rootDevice.Location.ToString();
@@ -497,7 +498,7 @@ namespace Rssdp.Infrastructure
values["DATE"] = DateTime.UtcNow.ToString("r");
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
values["LOCATION"] = rootDevice.Location.ToString();
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["NTS"] = "ssdp:alive";
values["NT"] = notificationType;
values["USN"] = uniqueServiceName;
@@ -522,7 +523,7 @@ namespace Rssdp.Infrastructure
}
tasks.Add(SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken));
- tasks.Add(SendByeByeNotification(device, String.Format("urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
+ tasks.Add(SendByeByeNotification(device, String.Format(CultureInfo.InvariantCulture, "urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
foreach (var childDevice in device.Devices)
{
@@ -542,7 +543,7 @@ namespace Rssdp.Infrastructure
// If needed later for non-server devices, these headers will need to be dynamic
values["HOST"] = "239.255.255.250:1900";
values["DATE"] = DateTime.UtcNow.ToString("r");
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["NTS"] = "ssdp:byebye";
values["NT"] = notificationType;
values["USN"] = uniqueServiceName;
@@ -550,7 +551,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
var sendCount = IsDisposed ? 1 : 3;
- WriteTrace(String.Format("Sent byebye notification"), device);
+ WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
diff --git a/bump_version b/bump_version
index 8012ec07f1..f615606e2d 100755
--- a/bump_version
+++ b/bump_version
@@ -21,7 +21,14 @@ fi
shared_version_file="./SharedVersion.cs"
build_file="./build.yaml"
# csproj files for nuget packages
-jellyfin_subprojects=( MediaBrowser.Common/MediaBrowser.Common.csproj Jellyfin.Data/Jellyfin.Data.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj MediaBrowser.Model/MediaBrowser.Model.csproj Emby.Naming/Emby.Naming.csproj )
+jellyfin_subprojects=(
+ MediaBrowser.Common/MediaBrowser.Common.csproj
+ Jellyfin.Data/Jellyfin.Data.csproj
+ MediaBrowser.Controller/MediaBrowser.Controller.csproj
+ MediaBrowser.Model/MediaBrowser.Model.csproj
+ Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+)
new_version="$1"
diff --git a/debian/control b/debian/control
index 51b20c670d..da9aa94d4d 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: misc
Priority: optional
Maintainer: Jellyfin Team
Build-Depends: debhelper (>= 9),
- dotnet-sdk-5.0,
+ dotnet-sdk-6.0,
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 326e995be6..178f94f719 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -2,7 +2,6 @@ FROM centos:7
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,12 +10,13 @@ ENV IS_DOCKER=YES
# Prepare CentOS environment
RUN yum update -yq \
&& yum install -yq epel-release \
- && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git
+ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
- && rpmdev-setuptree \
- && yum install -yq dotnet-sdk-${SDK_VERSION}
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5fcb98bb-21e1-47a5-bb8e-bb25f41a3e52/04811d5d05b7e694f040d2a13c1aae4c/dotnet-sdk-6.0.100-rc.1.21463.6-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 23b662526e..daba0eb7d1 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index a33099031e..db4e7f8178 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
@@ -18,16 +18,16 @@ RUN apt-get update -yqq \
RUN dpkg --add-architecture arm64 \
&& apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 8 \
- && cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \
+ && TARGET_LIST="arm64" cross-gcc-gensource 9 \
+ && cd cross-gcc-packages-amd64/cross-gcc-9-arm64 \
&& apt-get install -yqq --no-install-recommends \
- gcc-8-source libstdc++-8-dev-arm64-cross \
+ gcc-9-source libstdc++-9-dev-arm64-cross \
binutils-aarch64-linux-gnu bison flex libtool \
gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip \
libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 \
libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 \
- libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-8-dev:arm64
+ libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-9-dev:arm64
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index bc5e3543fa..9b008e7fb0 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
@@ -18,17 +18,17 @@ RUN apt-get update -yqq \
RUN dpkg --add-architecture armhf \
&& apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 8 \
- && cd cross-gcc-packages-amd64/cross-gcc-8-armhf \
+ && TARGET_LIST="armhf" cross-gcc-gensource 9 \
+ && cd cross-gcc-packages-amd64/cross-gcc-9-armhf \
&& apt-get install -yqq --no-install-recommends\
- gcc-8-source libstdc++-8-dev-armhf-cross \
+ gcc-9-source libstdc++-9-dev-armhf-cross \
binutils-aarch64-linux-gnu bison flex libtool gdb \
sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
systemtap-sdt-dev autogen expect chrpath zlib1g-dev \
zip binutils-arm-linux-gnueabihf libc6-dev:armhf \
linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \
libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf \
- liblttng-ust0:armhf libstdc++-8-dev:armhf
+ liblttng-ust0:armhf libstdc++-9-dev:armhf
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
index 0b1a57014f..b2bd40713d 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index 583f53ca09..fc60f16246 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index 177c117134..f5cc47d83e 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 590cde1677..f0f2977a40 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -2,7 +2,6 @@ FROM fedora:33
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -10,10 +9,14 @@ ENV IS_DOCKER=YES
# Prepare Fedora environment
RUN dnf update -yq \
- && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd
+ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget
# Install DotNET SDK
-RUN dnf install -yq dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5fcb98bb-21e1-47a5-bb8e-bb25f41a3e52/04811d5d05b7e694f040d2a13c1aae4c/dotnet-sdk-6.0.100-rc.1.21463.6-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 3c7e2b87f6..2c7e41cace 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index 3cda9ad235..e903cf1d32 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index ddf97cbd1c..0dd3c5e4e8 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 49e1c7bbf5..16a8218e18 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index fad44ef833..699ab2d408 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 90cc0717b7..b567d7bcea 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index d88efcdc95..fe1b4981bc 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -18,8 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/13b9d84c-a35b-4ffe-8f62-447a01403d64/1f9ae31daa0f7d98513e7551246899f2/dotnet-sdk-5.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5fcb98bb-21e1-47a5-bb8e-bb25f41a3e52/04811d5d05b7e694f040d2a13c1aae4c/dotnet-sdk-6.0.100-rc.1.21463.6-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 4f41bba2d9..d984f5d898 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -17,8 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/13b9d84c-a35b-4ffe-8f62-447a01403d64/1f9ae31daa0f7d98513e7551246899f2/dotnet-sdk-5.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5fcb98bb-21e1-47a5-bb8e-bb25f41a3e52/04811d5d05b7e694f040d2a13c1aae4c/dotnet-sdk-6.0.100-rc.1.21463.6-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 01752d5367..c013e6797e 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -17,8 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/13b9d84c-a35b-4ffe-8f62-447a01403d64/1f9ae31daa0f7d98513e7551246899f2/dotnet-sdk-5.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5fcb98bb-21e1-47a5-bb8e-bb25f41a3e52/04811d5d05b7e694f040d2a13c1aae4c/dotnet-sdk-6.0.100-rc.1.21463.6-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index acd0e18549..b9543a7c91 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64
index 69f0cadcfe..bfdc6e591e 100755
--- a/deployment/build.centos.amd64
+++ b/deployment/build.centos.amd64
@@ -8,6 +8,16 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+ pushd fedora
+
+ cp -a jellyfin.spec /tmp/spec.orig
+ sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
+
+ popd
+fi
+
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
@@ -37,4 +47,13 @@ fi
rm -f fedora/jellyfin*.tar.gz
+if [[ ${IS_DOCKER} == YES ]]; then
+ pushd fedora
+
+ cp -a /tmp/spec.orig jellyfin.spec
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+
+ popd
+fi
+
popd
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index 145e28d871..b2bbf9c29e 100755
--- a/deployment/build.debian.amd64
+++ b/deployment/build.debian.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
index 5699133a01..02f84471e6 100755
--- a/deployment/build.debian.arm64
+++ b/deployment/build.debian.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
index 20af2ddfbe..92779cb594 100755
--- a/deployment/build.debian.armhf
+++ b/deployment/build.debian.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
index 2c7bff5068..23c5ed86a2 100755
--- a/deployment/build.fedora.amd64
+++ b/deployment/build.fedora.amd64
@@ -8,6 +8,16 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+ pushd fedora
+
+ cp -a jellyfin.spec /tmp/spec.orig
+ sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
+
+ popd
+fi
+
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
@@ -37,4 +47,13 @@ fi
rm -f fedora/jellyfin*.tar.gz
+if [[ ${IS_DOCKER} == YES ]]; then
+ pushd fedora
+
+ cp -a /tmp/spec.orig jellyfin.spec
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+
+ popd
+fi
+
popd
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
index 0c29286c02..c36978c9e3 100755
--- a/deployment/build.ubuntu.amd64
+++ b/deployment/build.ubuntu.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
index 65d67f80f7..76d51e321f 100755
--- a/deployment/build.ubuntu.arm64
+++ b/deployment/build.ubuntu.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
index 370370abc1..0ff5ab0662 100755
--- a/deployment/build.ubuntu.armhf
+++ b/deployment/build.ubuntu.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 0d606f9f73..47dee7c13f 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/
-BuildRequires: dotnet-runtime-5.0, dotnet-sdk-5.0
+BuildRequires: dotnet-runtime-6.0, dotnet-sdk-6.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
# Disable Automatic Dependency Processing
AutoReqProv: no
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 981b796e03..3d9538d1bb 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -1,17 +1,28 @@
- net5.0
+ net6.0
false
true
+ true
+ true
+ true
+ snupkg
Jellyfin Contributors
+ Jellyfin.Extensions
+ 10.8.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index c39805aa35..3c7d504b10 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -44,7 +44,7 @@ namespace Jellyfin.Extensions.Json.Converters
{
try
{
- parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
+ parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()) ?? throw new FormatException();
convertedCount++;
}
catch (FormatException)
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index acc695ed2f..3a77072539 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -27,5 +27,39 @@ namespace Jellyfin.Extensions
return count;
}
+
+ ///
+ /// Returns the part on the left of the needle.
+ ///
+ /// The string to seek.
+ /// The needle to find.
+ /// The part left of the .
+ public static ReadOnlySpan LeftPart(this ReadOnlySpan haystack, char needle)
+ {
+ var pos = haystack.IndexOf(needle);
+ return pos == -1 ? haystack : haystack[..pos];
+ }
+
+ ///
+ /// Returns the part on the right of the needle.
+ ///
+ /// The string to seek.
+ /// The needle to find.
+ /// The part right of the .
+ public static ReadOnlySpan RightPart(this ReadOnlySpan haystack, char needle)
+ {
+ var pos = haystack.LastIndexOf(needle);
+ if (pos == -1)
+ {
+ return haystack;
+ }
+
+ if (pos == haystack.Length - 1)
+ {
+ return ReadOnlySpan.Empty;
+ }
+
+ return haystack[(pos + 1)..];
+ }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 0c36e81cca..b52ea078af 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
@@ -15,7 +15,7 @@
-
+
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 1619fa89c8..1fe4e25656 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index a5778b59c8..e9a9515710 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 5a48631c29..1fb95aab4d 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 20680157f5..2dc4ac19a5 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
index d1aa2e4764..7186cc0236 100644
--- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -14,5 +14,28 @@ namespace Jellyfin.Extensions.Tests
{
Assert.Equal(count, str.AsSpan().Count(needle));
}
+
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "Banana")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ [InlineData("Banana split 2", ' ', "Banana")]
+ public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().LeftPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "split")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ [InlineData("Banana split.", '.', "")]
+ [InlineData("Banana split 2", ' ', "2")]
+ public void RightPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().RightPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 7ea5039138..201f63a2d6 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
@@ -18,10 +18,14 @@
+
+
+
+
+
-
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index d002d5a347..d0d472e4d4 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Album", res.Album);
Assert.Equal(2021, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
}
[Fact]
@@ -85,7 +85,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("City to City", res.Album);
Assert.Equal(1978, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Contains("Electronic", res.Genres);
Assert.Contains("Ambient", res.Genres);
Assert.Contains("Pop", res.Genres);
@@ -105,7 +105,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Eyes wide open", res.Album);
Assert.Equal(2020, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Equal(22, res.People.Length);
Assert.Equal("Krysta Youngs", res.People[0].Name);
Assert.Equal(PersonType.Composer, res.People[0].Type);
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index 537a944b03..c07c9ea7db 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -31,5 +31,27 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
}
}
+
+ [Fact]
+ public void Parse_EmptyNewlineBetweenText_Success()
+ {
+ using (var stream = File.OpenRead("Test Data/example2.srt"))
+ {
+ var parsed = new SrtParser(new NullLogger()).Parse(stream, CancellationToken.None);
+ Assert.Equal(2, parsed.TrackEvents.Count);
+
+ var trackEvent1 = parsed.TrackEvents[0];
+ Assert.Equal("311", trackEvent1.Id);
+ Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
+ Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text);
+
+ var trackEvent2 = parsed.TrackEvents[1];
+ Assert.Equal("312", trackEvent2.Id);
+ Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
+ Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text);
+ }
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
new file mode 100644
index 0000000000..5fe2c84471
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class SubtitleEncoderTests
+ {
+ internal static TheoryData GetReadableFile_Valid_TestData()
+ {
+ var data = new TheoryData();
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ass",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ssa",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.srt",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.Http
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ass",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetReadableFile_Valid_TestData))]
+ internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo)
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ var subtitleEncoder = fixture.Create();
+ var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(subtitleInfo.Path, result.Path);
+ Assert.Equal(subtitleInfo.Protocol, result.Protocol);
+ Assert.Equal(subtitleInfo.Format, result.Format);
+ Assert.Equal(subtitleInfo.IsExternal, result.IsExternal);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt
new file mode 100644
index 0000000000..b14aa8ea34
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt
@@ -0,0 +1,11 @@
+311
+00:16:46,465 --> 00:16:49,009
+Una vez que la gente se entere
+
+de que ustedes están aquí,
+
+312
+00:16:49,092 --> 00:16:51,470
+este lugar se convertirá
+
+en un maldito zoológico.
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 09b8a7a948..a37e5ac920 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index a4ebab141e..75d466198a 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 5fa2ecfe9e..75d9b9ea90 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index d9e33617bc..0b2db64b0b 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 9b6ab7bdf5..5ecd846047 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -6,7 +6,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
Jellyfin.Server.Implementations.Tests
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
index 829be4cdb7..f12681b593 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
@@ -144,6 +144,22 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
new Episode { ParentIndexNumber = 1, IndexNumber = 3 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1);
+
+ // Premiere Date
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ 0);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ -1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
+ 1);
}
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
index 732b4f050d..2361e4aa4e 100644
--- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -21,6 +21,7 @@ namespace Jellyfin.Server.Integration.Tests
[InlineData("a=1", "a=1")] // won't be processed as it has a value
[InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
[InlineData("a=b&a=c", "a=b")]
+ [InlineData("a%3D1", "a=1")]
[InlineData("a%3Db%26a%3Dc", "a=b")]
public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 592b444c99..7939c7118e 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -1,6 +1,6 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
@@ -9,7 +9,7 @@
-
+
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index f249be674c..b30e690a5e 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset
@@ -10,7 +10,7 @@
-
+
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
index 419afb2dc4..d15c9d6f54 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -12,9 +12,6 @@ namespace Jellyfin.Server.Tests
{
[Theory]
[InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
- [InlineData("random+test", "random test")] // encoded
- [InlineData("random%20test", "random test")] // encoded
- [InlineData("++", " ")] // encoded
public static void EmptyValueTest(string query, string key)
{
var dict = new Dictionary
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index e085907583..94294c8bf3 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
false
../jellyfin-tests.ruleset