Merge remote-tracking branch 'upstream/master' into bye-tvdb

This commit is contained in:
crobibero 2020-11-15 13:12:43 -07:00
commit 4b15284324
142 changed files with 584 additions and 389 deletions

View file

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest" default: "ubuntu-latest"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 3.1.100 default: 5.0.100
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck

View file

@ -1,7 +1,7 @@
parameters: parameters:
LinuxImage: 'ubuntu-latest' LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 3.1.100 DotNetSdkVersion: 5.0.100
jobs: jobs:
- job: Build - job: Build

View file

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

View file

@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects - name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj' value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion - name: DotNetSdkVersion
value: 3.1.100 value: 5.0.100
pr: pr:
autoCancel: true autoCancel: true

View file

@ -1,4 +1,4 @@
ARG DOTNET_VERSION=3.1 ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& yarn install \ && yarn install \
&& mv dist /dist && mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View file

@ -2,7 +2,7 @@
##################################### #####################################
# Requires binfm_misc registration # Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1 ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder FROM node:alpine as web-builder
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist && mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View file

@ -2,7 +2,7 @@
##################################### #####################################
# Requires binfm_misc registration # Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1 ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder FROM node:alpine as web-builder
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist && mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View file

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View file

@ -31,7 +31,7 @@ namespace DvdLib.Ifo
continue; continue;
} }
var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{ {
ReadVTS(ifoNumber, ifo.FullName); ReadVTS(ifoNumber, ifo.FullName);

View file

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

View file

@ -17,7 +17,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View file

@ -83,7 +83,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header)) if (!string.IsNullOrEmpty(header))
{ {
// Starts with SECOND- // Starts with SECOND-
header = header.Split('-').Last(); header = header.Split('-')[^1];
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val)) if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
{ {

View file

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

View file

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

View file

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

View file

@ -83,7 +83,7 @@ namespace Emby.Notifications
return Task.CompletedTask; return Task.CompletedTask;
} }
private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e) private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
{ {
var type = NotificationType.ServerRestartRequired.ToString(); var type = NotificationType.ServerRestartRequired.ToString();
@ -99,7 +99,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false); await SendNotification(notification, null).ConfigureAwait(false);
} }
private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{ {
var entry = e.Argument; var entry = e.Argument;
@ -132,7 +132,7 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications"); return _config.GetConfiguration<NotificationOptions>("notifications");
} }
private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e) private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
{ {
if (!_appHost.HasUpdateAvailable) if (!_appHost.HasUpdateAvailable)
{ {
@ -151,7 +151,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false); await SendNotification(notification, null).ConfigureAwait(false);
} }
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{ {
if (!FilterItem(e.Item)) if (!FilterItem(e.Item))
{ {
@ -197,7 +197,7 @@ namespace Emby.Notifications
return item.SourceType == SourceType.Library; return item.SourceType == SourceType.Library;
} }
private async void LibraryUpdateTimerCallback(object state) private async void LibraryUpdateTimerCallback(object? state)
{ {
List<BaseItem> items; List<BaseItem> items;

View file

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

View file

@ -133,6 +133,33 @@ namespace Emby.Server.Implementations.AppBase
} }
} }
/// <summary>
/// Manually pre-loads a factory so that it is available pre system initialisation.
/// </summary>
/// <typeparam name="T">Class to register.</typeparam>
public virtual void RegisterConfiguration<T>()
where T : IConfigurationFactory
{
IConfigurationFactory factory = Activator.CreateInstance<T>();
if (_configurationFactories == null)
{
_configurationFactories = new[] { factory };
}
else
{
var oldLen = _configurationFactories.Length;
var arr = new IConfigurationFactory[oldLen + 1];
_configurationFactories.CopyTo(arr, 0);
arr[oldLen] = factory;
_configurationFactories = arr;
}
_configurationStores = _configurationFactories
.SelectMany(i => i.GetConfigurations())
.ToArray();
}
/// <summary> /// <summary>
/// Adds parts. /// Adds parts.
/// </summary> /// </summary>

View file

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase namespace Emby.Server.Implementations.AppBase
@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
} }
catch (Exception) catch (Exception)
{ {
configuration = Activator.CreateInstance(type); configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
} }
using var stream = new MemoryStream(buffer?.Length ?? 0); using var stream = new MemoryStream(buffer?.Length ?? 0);
@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
// If the file didn't exist before, or if something has changed, re-save // If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer)) if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{ {
Directory.CreateDirectory(Path.GetDirectoryName(path)); var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
// Save it after load in case we got new items // Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {

View file

@ -125,7 +125,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory; private IHttpClientFactory _httpClientFactory;
private string[] _urlPrefixes; private string[] _urlPrefixes;
/// <summary> /// <summary>
@ -496,24 +495,11 @@ namespace Emby.Server.Implementations
HttpsPort = ServerConfiguration.DefaultHttpsPort; HttpsPort = ServerConfiguration.DefaultHttpsPort;
} }
if (Plugins != null)
{
var pluginBuilder = new StringBuilder();
foreach (var plugin in Plugins)
{
pluginBuilder.Append(plugin.Name)
.Append(' ')
.Append(plugin.Version)
.AppendLine();
}
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
}
DiscoverTypes(); DiscoverTypes();
RegisterServices(); RegisterServices();
RegisterPluginServices();
} }
/// <summary> /// <summary>
@ -777,10 +763,24 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_plugins = GetExports<IPlugin>() _plugins = GetExports<IPlugin>()
.Select(LoadPlugin)
.Where(i => i != null) .Where(i => i != null)
.ToArray(); .ToArray();
if (Plugins != null)
{
var pluginBuilder = new StringBuilder();
foreach (var plugin in Plugins)
{
pluginBuilder.Append(plugin.Name)
.Append(' ')
.Append(plugin.Version)
.AppendLine();
}
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
}
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
Resolve<ILibraryManager>().AddParts( Resolve<ILibraryManager>().AddParts(
@ -810,21 +810,6 @@ namespace Emby.Server.Implementations
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
} }
private IPlugin LoadPlugin(IPlugin plugin)
{
try
{
plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
return null;
}
return plugin;
}
/// <summary> /// <summary>
/// Discovers the types. /// Discovers the types.
/// </summary> /// </summary>
@ -835,6 +820,22 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
} }
private void RegisterPluginServices()
{
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
{
try
{
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
instance.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
}
}
}
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies) private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{ {
foreach (var ass in assemblies) foreach (var ass in assemblies)
@ -994,6 +995,12 @@ namespace Emby.Server.Implementations
{ {
var minimumVersion = new Version(0, 0, 0, 1); var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>(); var versions = new List<LocalPlugin>();
if (!Directory.Exists(path))
{
// Plugin path doesn't exist, don't try to enumerate subfolders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories) foreach (var dir in directories)
@ -1024,7 +1031,7 @@ namespace Emby.Server.Implementations
else else
{ {
// No metafile, so lets see if the folder is versioned. // No metafile, so lets see if the folder is versioned.
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_'); int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion)) if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))

View file

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using static MediaBrowser.Common.Cryptography.Constants; using static MediaBrowser.Common.Cryptography.Constants;
@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Cryptography
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
} }
using var h = HashAlgorithm.Create(hashMethod); using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
if (salt.Length == 0) if (salt.Length == 0)
{ {
return h.ComputeHash(bytes); return h.ComputeHash(bytes);

View file

@ -1007,7 +1007,7 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts) foreach (var part in parts)
{ {
@ -1057,7 +1057,7 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
var list = new List<ItemImageInfo>(); var list = new List<ItemImageInfo>();
foreach (var part in parts) foreach (var part in parts)
{ {
@ -1096,7 +1096,7 @@ namespace Emby.Server.Implementations.Data
public ItemImageInfo ItemImageInfoFromValueString(string value) public ItemImageInfo ItemImageInfoFromValueString(string value)
{ {
var parts = value.Split(new[] { '*' }, StringSplitOptions.None); var parts = value.Split('*', StringSplitOptions.None);
if (parts.Length < 3) if (parts.Length < 3)
{ {
@ -1532,7 +1532,7 @@ namespace Emby.Server.Implementations.Data
{ {
if (!reader.IsDBNull(index)) if (!reader.IsDBNull(index))
{ {
item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
} }
index++; index++;
@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Data
{ {
IEnumerable<MetadataField> GetLockedFields(string s) IEnumerable<MetadataField> GetLockedFields(string s)
{ {
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
{ {
if (Enum.TryParse(i, true, out MetadataField parsedValue)) if (Enum.TryParse(i, true, out MetadataField parsedValue))
{ {
@ -1612,7 +1612,7 @@ namespace Emby.Server.Implementations.Data
{ {
if (!reader.IsDBNull(index)) if (!reader.IsDBNull(index))
{ {
item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
} }
index++; index++;
@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data
{ {
if (!reader.IsDBNull(index)) if (!reader.IsDBNull(index))
{ {
item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
} }
index++; index++;
@ -1636,7 +1636,7 @@ namespace Emby.Server.Implementations.Data
{ {
IEnumerable<TrailerType> GetTrailerTypes(string s) IEnumerable<TrailerType> GetTrailerTypes(string s)
{ {
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
{ {
if (Enum.TryParse(i, true, out TrailerType parsedValue)) if (Enum.TryParse(i, true, out TrailerType parsedValue))
{ {
@ -1811,7 +1811,7 @@ namespace Emby.Server.Implementations.Data
{ {
if (!reader.IsDBNull(index)) if (!reader.IsDBNull(index))
{ {
item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
} }
index++; index++;
@ -1848,14 +1848,14 @@ namespace Emby.Server.Implementations.Data
{ {
if (item is IHasArtist hasArtists && !reader.IsDBNull(index)) if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
{ {
hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
} }
index++; index++;
if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index)) if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
{ {
hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
} }
index++; index++;
@ -5611,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return counts; return counts;
} }
var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToLookup(x => x); .ToLookup(x => x);
foreach (var type in allTypes) foreach (var type in allTypes)

View file

@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Dto
continue; continue;
} }
var containers = container.Split(new[] { ',' }); var containers = container.Split(',');
if (containers.Length < 2) if (containers.Length < 2)
{ {
continue; continue;

View file

@ -32,13 +32,13 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Mono.Nat" Version="3.0.0" /> <PackageReference Include="Mono.Nat" Version="3.0.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" />
<PackageReference Include="sharpcompress" Version="0.26.0" /> <PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.0" />
@ -49,10 +49,12 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View file

@ -245,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
return null; return null;
} }
var parts = authorizationHeader.Split(new[] { ' ' }, 2); var parts = authorizationHeader.Split(' ', 2);
// There should be at least to parts // There should be at least to parts
if (parts.Length != 2) if (parts.Length != 2)
@ -269,11 +269,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
foreach (var item in parts) foreach (var item in parts)
{ {
var param = item.Trim().Split(new[] { '=' }, 2); var param = item.Trim().Split('=', 2);
if (param.Length == 2) if (param.Length == 2)
{ {
var value = NormalizeValue(param[1].Trim(new[] { '"' })); var value = NormalizeValue(param[1].Trim('"'));
result[param[0]] = value; result[param[0]] = value;
} }
} }

View file

@ -2705,7 +2705,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren); var videos = videoListResolver.Resolve(fileSystemChildren);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null) if (currentVideo != null)
{ {

View file

@ -849,7 +849,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Key can't be empty.", nameof(key)); throw new ArgumentException("Key can't be empty.", nameof(key));
} }
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2); var keys = key.Split(LiveStreamIdDelimeter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));

View file

@ -201,7 +201,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue; continue;
} }
var firstMedia = resolvedItem.Files.First(); var firstMedia = resolvedItem.Files[0];
var libraryItem = new T var libraryItem = new T
{ {

View file

@ -1429,7 +1429,7 @@ namespace Emby.Server.Implementations.LiveTv
return result; return result;
} }
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, ItemFields[] fields, User user = null) public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
{ {
var programTuples = new List<Tuple<BaseItemDto, string, string>>(); var programTuples = new List<Tuple<BaseItemDto, string, string>>();
var hasChannelImage = fields.Contains(ItemFields.ChannelImage); var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
@ -2208,7 +2208,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <returns>Task.</returns> /// <returns>Task.</returns>
public Task ResetTuner(string id, CancellationToken cancellationToken) public Task ResetTuner(string id, CancellationToken cancellationToken)
{ {
var parts = id.Split(new[] { '_' }, 2); var parts = id.Split('_', 2);
var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase)); var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase));

View file

@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (string.IsNullOrEmpty(currentFile)) if (string.IsNullOrEmpty(currentFile))
{ {
return (files.Last(), true); return (files[^1], true);
} }
var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1; var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;

View file

@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
{ {
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
string numberString = null; string numberString = null;
@ -273,8 +273,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private static string GetChannelName(string extInf, Dictionary<string, string> attributes) private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
{ {
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null; var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null;
// Check for channel number with the format from SatIp // Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84. VOX Schweiz

View file

@ -113,5 +113,7 @@
"TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
"TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.", "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
"TaskRefreshChannels": "Csatornák frissítése", "TaskRefreshChannels": "Csatornák frissítése",
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat." "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
"TaskCleanActivityLog": "Tevékenységnapló törlése"
} }

View file

@ -112,5 +112,7 @@
"TasksChannelsCategory": "Canale de pe Internet", "TasksChannelsCategory": "Canale de pe Internet",
"TasksApplicationCategory": "Aplicație", "TasksApplicationCategory": "Aplicație",
"TasksLibraryCategory": "Librărie", "TasksLibraryCategory": "Librărie",
"TasksMaintenanceCategory": "Mentenanță" "TasksMaintenanceCategory": "Mentenanță",
"TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
"TaskCleanActivityLog": "Curăță Jurnalul de Activitate"
} }

View file

@ -112,5 +112,7 @@
"TasksChannelsCategory": "Интернет канали", "TasksChannelsCategory": "Интернет канали",
"TasksApplicationCategory": "Апликација", "TasksApplicationCategory": "Апликација",
"TasksLibraryCategory": "Библиотека", "TasksLibraryCategory": "Библиотека",
"TasksMaintenanceCategory": "Одржавање" "TasksMaintenanceCategory": "Одржавање",
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
"TaskCleanActivityLog": "Очисти историју активности"
} }

View file

@ -112,5 +112,7 @@
"UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்", "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
"HomeVideos": "முகப்பு வீடியோக்கள்", "HomeVideos": "முகப்பு வீடியோக்கள்",
"UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது", "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது" "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது",
"TaskCleanActivityLogDescription": "உள்ளமைக்கப்பட்ட வயதை விட பழைய செயல்பாட்டு பதிவு உள்ளீடுகளை நீக்குகிறது.",
"TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி"
} }

View file

@ -653,7 +653,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
try try
{ {
_logger.LogInformation(Name + ": Waiting on Task"); _logger.LogInformation(Name + ": Waiting on Task");
var exited = Task.WaitAll(new[] { task }, 2000); var exited = task.Wait(2000);
if (exited) if (exited)
{ {

View file

@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
try try
{ {
previouslyFailedImages = File.ReadAllText(failHistoryPath) previouslyFailedImages = File.ReadAllText(failHistoryPath)
.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToList(); .ToList();
} }
catch (IOException) catch (IOException)

View file

@ -8,6 +8,7 @@ using System.Linq;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
@ -55,9 +56,9 @@ namespace Emby.Server.Implementations.Session
connection.Closed += OnConnectionClosed; connection.Closed += OnConnectionClosed;
} }
private void OnConnectionClosed(object sender, EventArgs e) private void OnConnectionClosed(object? sender, EventArgs e)
{ {
var connection = (IWebSocketConnection)sender; var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id); _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
_sockets.Remove(connection); _sockets.Remove(connection);
connection.Closed -= OnConnectionClosed; connection.Closed -= OnConnectionClosed;

View file

@ -14,6 +14,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
@ -1347,7 +1348,9 @@ namespace Jellyfin.Api.Controllers
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer); var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
@ -1565,8 +1568,7 @@ namespace Jellyfin.Api.Controllers
private string GetSegmentPath(StreamState state, string playlist, int index) private string GetSegmentPath(StreamState state, string playlist, int index)
{ {
var folder = Path.GetDirectoryName(playlist); var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
var filename = Path.GetFileNameWithoutExtension(playlist); var filename = Path.GetFileNameWithoutExtension(playlist);
return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer)); return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.EnvironmentDtos; using Jellyfin.Api.Models.EnvironmentDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -103,6 +104,11 @@ namespace Jellyfin.Api.Controllers
if (validatePathDto.ValidateWritable) if (validatePathDto.ValidateWritable)
{ {
if (validatePathDto.Path == null)
{
throw new ResourceNotFoundException(nameof(validatePathDto.Path));
}
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString()); var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
try try
{ {

View file

@ -78,8 +78,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
User = user, User = user,
MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
Recursive = true, Recursive = true,
EnableTotalRecordCount = false, EnableTotalRecordCount = false,
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers
var genreQuery = new InternalItemsQuery(user) var genreQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = IncludeItemTypes =
(includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
{ {
Fields = Array.Empty<ItemFields>(), Fields = Array.Empty<ItemFields>(),

View file

@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
return _dtoService.GetBaseItemDto(item, dtoOptions); return _dtoService.GetBaseItemDto(item, dtoOptions);
} }
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions) private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
where T : BaseItem, new() where T : BaseItem, new()
{ {
var result = libraryManager.GetItemList(new InternalItemsQuery var result = libraryManager.GetItemList(new InternalItemsQuery

View file

@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -134,7 +135,8 @@ namespace Jellyfin.Api.Controllers
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath) var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
.FirstOrDefault(i => .FirstOrDefault(i =>
string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
&& i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1); && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid.");
return GetFileResult(file, playlistPath); return GetFileResult(file, playlistPath);
} }

View file

@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="theme">Theme to search.</param> /// <param name="theme">Theme to search.</param>
/// <param name="name">File name to search for.</param> /// <param name="name">File name to search for.</param>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
private ActionResult GetImageFile(string basePath, string? theme, string? name) private ActionResult GetImageFile(string basePath, string theme, string? name)
{ {
var themeFolder = Path.Combine(basePath, theme); var themeFolder = Path.Combine(basePath, theme);
if (Directory.Exists(themeFolder)) if (Directory.Exists(themeFolder))

View file

@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
@ -1268,7 +1269,7 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(key, value); Response.Headers.Add(key, value);
} }
Response.ContentType = imageContentType; Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);

View file

@ -334,10 +334,16 @@ namespace Jellyfin.Api.Controllers
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
{ {
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
if (result.Content.Headers.ContentType?.MediaType == null)
{
throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
}
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1]; var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext); var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(directory);
using (var stream = result.Content) using (var stream = result.Content)
{ {
await using var fileStream = new FileStream( await using var fileStream = new FileStream(
@ -351,7 +357,9 @@ namespace Jellyfin.Api.Controllers
await stream.CopyToAsync(fileStream).ConfigureAwait(false); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
Directory.CreateDirectory(pointerCacheDirectory);
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
} }

View file

@ -456,7 +456,7 @@ namespace Jellyfin.Api.Controllers
: null; : null;
var dtoOptions = new DtoOptions().AddClientFields(Request); var dtoOptions = new DtoOptions().AddClientFields(Request);
BaseItem parent = item.GetParent(); BaseItem? parent = item.GetParent();
while (parent != null) while (parent != null)
{ {
@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
parent = parent.GetParent(); parent = parent?.GetParent();
} }
return baseItemDtos; return baseItemDtos;
@ -681,12 +681,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <response code="200">Similar items returned.</response> /// <response code="200">Similar items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
[HttpGet("Artists/{itemId}/Similar")] [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
[HttpGet("Items/{itemId}/Similar")] [HttpGet("Items/{itemId}/Similar")]
[HttpGet("Albums/{itemId}/Similar")] [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
[HttpGet("Shows/{itemId}/Similar")] [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
[HttpGet("Movies/{itemId}/Similar")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
[HttpGet("Trailers/{itemId}/Similar")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@ -893,7 +893,7 @@ namespace Jellyfin.Api.Controllers
return _libraryManager.GetItemsResult(query).TotalRecordCount; return _libraryManager.GetItemsResult(query).TotalRecordCount;
} }
private BaseItem TranslateParentItem(BaseItem item, User user) private BaseItem? TranslateParentItem(BaseItem item, User user)
{ {
return item.GetParent() is AggregateFolder return item.GetParent() is AggregateFolder
? _libraryManager.GetUserRootFolder().GetChildren(user, true) ? _libraryManager.GetUserRootFolder().GetChildren(user, true)

View file

@ -1073,7 +1073,7 @@ namespace Jellyfin.Api.Controllers
var client = _httpClientFactory.CreateClient(NamedClient.Default); var client = _httpClientFactory.CreateClient(NamedClient.Default);
// https://json.schedulesdirect.org/20141201/available/countries // https://json.schedulesdirect.org/20141201/available/countries
// Can't dispose the response as it's required up the call chain. // Can't dispose the response as it's required up the call chain.
var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries") var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
.ConfigureAwait(false); .ConfigureAwait(false);
return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json); return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);

View file

@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
{ {
var dtoOptions = new DtoOptions().AddClientFields(Request); var dtoOptions = new DtoOptions().AddClientFields(Request);
MusicGenre item; MusicGenre? item;
if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
{ {
@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
return _dtoService.GetBaseItemDto(item, dtoOptions); return _dtoService.GetBaseItemDto(item, dtoOptions);
} }
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions) private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
where T : BaseItem, new() where T : BaseItem, new()
{ {
var result = libraryManager.GetItemList(new InternalItemsQuery var result = libraryManager.GetItemList(new InternalItemsQuery

View file

@ -54,6 +54,11 @@ namespace Jellyfin.Api.Controllers
string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)) string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
.FirstOrDefault(); .FirstOrDefault();
if (result == null)
{
return NotFound();
}
return result; return result;
} }
@ -149,12 +154,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="repositoryInfos">The list of package repositories.</param> /// <param name="repositoryInfos">The list of package repositories.</param>
/// <response code="204">Package repositories saved.</response> /// <response code="204">Package repositories saved.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpOptions("Repositories")] [HttpPost("Repositories")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos) public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
{ {
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
_serverConfigurationManager.SaveConfiguration();
return NoContent(); return NoContent();
} }
} }

View file

@ -157,9 +157,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl) public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
{ {
var urlHash = imageUrl.GetMD5(); var urlHash = imageUrl.ToString().GetMD5();
var pointerCachePath = GetFullCachePath(urlHash.ToString()); var pointerCachePath = GetFullCachePath(urlHash.ToString());
string? contentPath = null; string? contentPath = null;
@ -245,17 +245,25 @@ namespace Jellyfin.Api.Controllers
/// <param name="urlHash">The URL hash.</param> /// <param name="urlHash">The URL hash.</param>
/// <param name="pointerCachePath">The pointer cache path.</param> /// <param name="pointerCachePath">The pointer cache path.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
{ {
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using var response = await httpClient.GetAsync(url).ConfigureAwait(false); using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last(); if (response.Content.Headers.ContentType?.MediaType == null)
{
throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
}
var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext); var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
Directory.CreateDirectory(pointerCacheDirectory);
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None) await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
} }

View file

@ -260,7 +260,7 @@ namespace Jellyfin.Api.Controllers
} }
} }
private T GetParentWithImage<T>(BaseItem item, ImageType type) private T? GetParentWithImage<T>(BaseItem item, ImageType type)
where T : BaseItem where T : BaseItem
{ {
return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type)); return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));

View file

@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)] [Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<FileStreamResult>> GetAttachment( public async Task<ActionResult> GetAttachment(
[FromRoute, Required] Guid videoId, [FromRoute, Required] Guid videoId,
[FromRoute, Required] string mediaSourceId, [FromRoute, Required] string mediaSourceId,
[FromRoute, Required] int index) [FromRoute, Required] int index)

View file

@ -11,6 +11,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
@ -361,7 +362,8 @@ namespace Jellyfin.Api.Controllers
var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts"; var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format; var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
var segmentFormat = format.TrimStart('.'); var segmentFormat = format.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))

View file

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -43,7 +45,7 @@ namespace Jellyfin.Api.Extensions
client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
{ {
int oldLen = dtoOptions.Fields.Length; int oldLen = dtoOptions.Fields.Count;
var arr = new ItemFields[oldLen + 1]; var arr = new ItemFields[oldLen + 1];
dtoOptions.Fields.CopyTo(arr, 0); dtoOptions.Fields.CopyTo(arr, 0);
arr[oldLen] = ItemFields.RecursiveItemCount; arr[oldLen] = ItemFields.RecursiveItemCount;
@ -61,7 +63,7 @@ namespace Jellyfin.Api.Extensions
client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
{ {
int oldLen = dtoOptions.Fields.Length; int oldLen = dtoOptions.Fields.Count;
var arr = new ItemFields[oldLen + 1]; var arr = new ItemFields[oldLen + 1];
dtoOptions.Fields.CopyTo(arr, 0); dtoOptions.Fields.CopyTo(arr, 0);
arr[oldLen] = ItemFields.ChildCount; arr[oldLen] = ItemFields.ChildCount;
@ -90,7 +92,7 @@ namespace Jellyfin.Api.Extensions
bool? enableImages, bool? enableImages,
bool? enableUserData, bool? enableUserData,
int? imageTypeLimit, int? imageTypeLimit,
ImageType[] enableImageTypes) IReadOnlyList<ImageType> enableImageTypes)
{ {
dtoOptions.EnableImages = enableImages ?? true; dtoOptions.EnableImages = enableImages ?? true;
@ -104,7 +106,7 @@ namespace Jellyfin.Api.Extensions
dtoOptions.EnableUserData = enableUserData.Value; dtoOptions.EnableUserData = enableUserData.Value;
} }
if (enableImageTypes.Length != 0) if (enableImageTypes.Count != 0)
{ {
dtoOptions.ImageTypes = enableImageTypes; dtoOptions.ImageTypes = enableImageTypes;
} }

View file

@ -1,8 +1,10 @@
using System.Net.Http; using System;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
@ -98,6 +100,11 @@ namespace Jellyfin.Api.Helpers
TranscodingJobType transcodingJobType, TranscodingJobType transcodingJobType,
StreamingRequestDto streamingRequest) StreamingRequestDto streamingRequest)
{ {
if (_httpContextAccessor.HttpContext == null)
{
throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
}
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head; bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();

View file

@ -113,7 +113,7 @@ namespace Jellyfin.Api.Helpers
StreamingRequestDto streamingRequest, StreamingRequestDto streamingRequest,
bool enableAdaptiveBitrateStreaming) bool enableAdaptiveBitrateStreaming)
{ {
var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head; var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal( return await GetMasterPlaylistInternal(
streamingRequest, streamingRequest,
@ -130,6 +130,11 @@ namespace Jellyfin.Api.Helpers
TranscodingJobType transcodingJobType, TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource) CancellationTokenSource cancellationTokenSource)
{ {
if (_httpContextAccessor.HttpContext == null)
{
throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
}
using var state = await StreamingHelpers.GetStreamingState( using var state = await StreamingHelpers.GetStreamingState(
streamingRequest, streamingRequest,
_httpContextAccessor.HttpContext.Request, _httpContextAccessor.HttpContext.Request,
@ -487,14 +492,14 @@ namespace Jellyfin.Api.Helpers
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{ {
string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); string? profile = state.GetRequestedProfiles("h264").FirstOrDefault();
return HlsCodecStringHelpers.GetH264String(profile, level); return HlsCodecStringHelpers.GetH264String(profile, level);
} }
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); string? profile = state.GetRequestedProfiles("h265").FirstOrDefault();
return HlsCodecStringHelpers.GetH265String(profile, level); return HlsCodecStringHelpers.GetH265String(profile, level);
} }

View file

@ -37,8 +37,8 @@ namespace Jellyfin.Api.Helpers
} }
// Can't dispose the response as it's required up the call chain. // Can't dispose the response as it's required up the call chain.
var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); var response = await httpClient.GetAsync(new Uri(state.MediaPath)).ConfigureAwait(false);
var contentType = response.Content.Headers.ContentType.ToString(); var contentType = response.Content.Headers.ContentType?.ToString();
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";

View file

@ -23,7 +23,7 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
/// <param name="profile">AAC profile.</param> /// <param name="profile">AAC profile.</param>
/// <returns>AAC codec string.</returns> /// <returns>AAC codec string.</returns>
public static string GetAACString(string profile) public static string GetAACString(string? profile)
{ {
StringBuilder result = new StringBuilder("mp4a", 9); StringBuilder result = new StringBuilder("mp4a", 9);
@ -46,7 +46,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="profile">H.264 profile.</param> /// <param name="profile">H.264 profile.</param>
/// <param name="level">H.264 level.</param> /// <param name="level">H.264 level.</param>
/// <returns>H.264 string.</returns> /// <returns>H.264 string.</returns>
public static string GetH264String(string profile, int level) public static string GetH264String(string? profile, int level)
{ {
StringBuilder result = new StringBuilder("avc1", 11); StringBuilder result = new StringBuilder("avc1", 11);
@ -80,7 +80,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="profile">H.265 profile.</param> /// <param name="profile">H.265 profile.</param>
/// <param name="level">H.265 level.</param> /// <param name="level">H.265 level.</param>
/// <returns>H.265 string.</returns> /// <returns>H.265 string.</returns>
public static string GetH265String(string profile, int level) public static string GetH265String(string? profile, int level)
{ {
// The h265 syntax is a bit of a mystery at the time this comment was written. // The h265 syntax is a bit of a mystery at the time this comment was written.
// This is what I've found through various sources: // This is what I've found through various sources:

View file

@ -45,6 +45,11 @@ namespace Jellyfin.Api.Helpers
while (!reader.EndOfStream) while (!reader.EndOfStream)
{ {
var line = await reader.ReadLineAsync().ConfigureAwait(false); var line = await reader.ReadLineAsync().ConfigureAwait(false);
if (line == null)
{
// Nothing currently in buffer.
break;
}
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
{ {

View file

@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -90,6 +91,11 @@ namespace Jellyfin.Api.Helpers
allowAsyncFileRead = true; 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); await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
var eofCount = 0; var eofCount = 0;

View file

@ -74,7 +74,7 @@ namespace Jellyfin.Api.Helpers
} }
return removeEmpty return removeEmpty
? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) ? value.Split(separator, StringSplitOptions.RemoveEmptyEntries)
: value.Split(separator); : value.Split(separator);
} }

View file

@ -83,8 +83,12 @@ namespace Jellyfin.Api.Helpers
} }
streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query); streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
if (httpRequest.Path.Value == null)
{
throw new ResourceNotFoundException(nameof(httpRequest.Path));
}
var url = httpRequest.Path.Value.Split('.').Last(); var url = httpRequest.Path.Value.Split('.')[^1];
if (string.IsNullOrEmpty(streamingRequest.AudioCodec)) if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
{ {

View file

@ -12,6 +12,7 @@ using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@ -102,7 +103,7 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
/// <param name="playSessionId">Playback session id.</param> /// <param name="playSessionId">Playback session id.</param>
/// <returns>The transcoding job.</returns> /// <returns>The transcoding job.</returns>
public TranscodingJobDto GetTranscodingJob(string playSessionId) public TranscodingJobDto? GetTranscodingJob(string playSessionId)
{ {
lock (_activeTranscodingJobs) lock (_activeTranscodingJobs)
{ {
@ -116,7 +117,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="path">Path to the transcoding file.</param> /// <param name="path">Path to the transcoding file.</param>
/// <param name="type">The <see cref="TranscodingJobType"/>.</param> /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
/// <returns>The transcoding job.</returns> /// <returns>The transcoding job.</returns>
public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type) public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
{ {
lock (_activeTranscodingJobs) lock (_activeTranscodingJobs)
{ {
@ -193,10 +194,9 @@ namespace Jellyfin.Api.Helpers
/// Called when [transcode kill timer stopped]. /// Called when [transcode kill timer stopped].
/// </summary> /// </summary>
/// <param name="state">The state.</param> /// <param name="state">The state.</param>
private async void OnTranscodeKillTimerStopped(object state) private async void OnTranscodeKillTimerStopped(object? state)
{ {
var job = (TranscodingJobDto)state; var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
if (!job.HasExited && job.Type != TranscodingJobType.Progressive) if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
{ {
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds; var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
@ -489,7 +489,8 @@ namespace Jellyfin.Api.Helpers
CancellationTokenSource cancellationTokenSource, CancellationTokenSource cancellationTokenSource,
string? workingDirectory = null) string? workingDirectory = null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
Directory.CreateDirectory(directory);
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false); await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
@ -523,7 +524,7 @@ namespace Jellyfin.Api.Helpers
RedirectStandardInput = true, RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath, FileName = _mediaEncoder.EncoderPath,
Arguments = commandLineArguments, Arguments = commandLineArguments,
WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory, WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
ErrorDialog = false ErrorDialog = false
}, },
EnableRaisingEvents = true EnableRaisingEvents = true
@ -827,7 +828,7 @@ namespace Jellyfin.Api.Helpers
{ {
lock (_transcodingLocks) lock (_transcodingLocks)
{ {
if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result)) if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
{ {
result = new SemaphoreSlim(1, 1); result = new SemaphoreSlim(1, 1);
_transcodingLocks[outputPath] = result; _transcodingLocks[outputPath] = result;
@ -837,7 +838,7 @@ namespace Jellyfin.Api.Helpers
} }
} }
private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e) private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{ {
if (!string.IsNullOrWhiteSpace(e.PlaySessionId)) if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{ {

View file

@ -6,17 +6,19 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
</ItemGroup> </ItemGroup>

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.ModelBinders namespace Jellyfin.Api.ModelBinders
{ {
@ -11,6 +13,17 @@ namespace Jellyfin.Api.ModelBinders
/// </summary> /// </summary>
public class CommaDelimitedArrayModelBinder : IModelBinder public class CommaDelimitedArrayModelBinder : IModelBinder
{ {
private readonly ILogger<CommaDelimitedArrayModelBinder> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
{
_logger = logger;
}
/// <inheritdoc/> /// <inheritdoc/>
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
@ -20,16 +33,8 @@ namespace Jellyfin.Api.ModelBinders
if (valueProviderResult.Length > 1) if (valueProviderResult.Length > 1)
{ {
var result = Array.CreateInstance(elementType, valueProviderResult.Length); var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
bindingContext.Result = ModelBindingResult.Success(typedValues);
for (int i = 0; i < valueProviderResult.Length; i++)
{
var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim());
result.SetValue(value, i);
}
bindingContext.Result = ModelBindingResult.Success(result);
} }
else else
{ {
@ -37,13 +42,8 @@ namespace Jellyfin.Api.ModelBinders
if (value != null) if (value != null)
{ {
var values = Array.ConvertAll( var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), var typedValues = GetParsedResult(splitValues, elementType, converter);
x => converter.ConvertFromString(x?.Trim()));
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Result = ModelBindingResult.Success(typedValues); bindingContext.Result = ModelBindingResult.Success(typedValues);
} }
else else
@ -55,5 +55,36 @@ namespace Jellyfin.Api.ModelBinders
return Task.CompletedTask; return Task.CompletedTask;
} }
private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
{
var parsedValues = new object?[values.Count];
var convertedCount = 0;
for (var i = 0; i < values.Count; i++)
{
try
{
parsedValues[i] = converter.ConvertFromString(values[i].Trim());
convertedCount++;
}
catch (FormatException e)
{
_logger.LogWarning(e, "Error converting value.");
}
}
var typedValues = Array.CreateInstance(elementType, convertedCount);
var typedValueIndex = 0;
for (var i = 0; i < parsedValues.Length; i++)
{
if (parsedValues[i] != null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
}
}
return typedValues;
}
} }
} }

View file

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System;
using System.Collections.Generic;
namespace Jellyfin.Api.Models.LibraryDtos namespace Jellyfin.Api.Models.LibraryDtos
{ {
@ -10,25 +11,21 @@ namespace Jellyfin.Api.Models.LibraryDtos
/// <summary> /// <summary>
/// Gets or sets the metadata savers. /// Gets or sets the metadata savers.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataSavers", Justification = "Imported from ServiceStack")] public IReadOnlyList<LibraryOptionInfoDto> MetadataSavers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
public LibraryOptionInfoDto[] MetadataSavers { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the metadata readers. /// Gets or sets the metadata readers.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataReaders", Justification = "Imported from ServiceStack")] public IReadOnlyList<LibraryOptionInfoDto> MetadataReaders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
public LibraryOptionInfoDto[] MetadataReaders { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the subtitle fetchers. /// Gets or sets the subtitle fetchers.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SubtitleFetchers", Justification = "Imported from ServiceStack")] public IReadOnlyList<LibraryOptionInfoDto> SubtitleFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
public LibraryOptionInfoDto[] SubtitleFetchers { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the type options. /// Gets or sets the type options.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "TypeOptions", Justification = "Imported from ServiceStack")] public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
public LibraryTypeOptionsDto[] TypeOptions { get; set; } = null!;
} }
} }

View file

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System;
using System.Collections.Generic;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -17,25 +18,21 @@ namespace Jellyfin.Api.Models.LibraryDtos
/// <summary> /// <summary>
/// Gets or sets the metadata fetchers. /// Gets or sets the metadata fetchers.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataFetchers", Justification = "Imported from ServiceStack")] public IReadOnlyList<LibraryOptionInfoDto> MetadataFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
public LibraryOptionInfoDto[] MetadataFetchers { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the image fetchers. /// Gets or sets the image fetchers.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "ImageFetchers", Justification = "Imported from ServiceStack")] public IReadOnlyList<LibraryOptionInfoDto> ImageFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
public LibraryOptionInfoDto[] ImageFetchers { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the supported image types. /// Gets or sets the supported image types.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SupportedImageTypes", Justification = "Imported from ServiceStack")] public IReadOnlyList<ImageType> SupportedImageTypes { get; set; } = Array.Empty<ImageType>();
public ImageType[] SupportedImageTypes { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the default image options. /// Gets or sets the default image options.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "DefaultImageOptions", Justification = "Imported from ServiceStack")] public IReadOnlyList<ImageOption> DefaultImageOptions { get; set; } = Array.Empty<ImageOption>();
public ImageOption[] DefaultImageOptions { get; set; } = null!;
} }
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -25,8 +26,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary> /// <summary>
/// Gets or sets list of mappings. /// Gets or sets list of mappings.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "Mappings", Justification = "Imported from ServiceStack")] public IReadOnlyList<NameValuePair> Mappings { get; set; } = Array.Empty<NameValuePair>();
public NameValuePair[] Mappings { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets provider name. /// Gets or sets provider name.

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using MediaBrowser.Common.Json.Converters; using MediaBrowser.Common.Json.Converters;
@ -143,8 +144,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// Optional. /// Optional.
/// </summary> /// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "EnableImageTypes", Justification = "Imported from ServiceStack")] public IReadOnlyList<ImageType> EnableImageTypes { get; set; } = Array.Empty<ImageType>();
public ImageType[] EnableImageTypes { get; set; } = Array.Empty<ImageType>();
/// <summary> /// <summary>
/// Gets or sets include user data. /// Gets or sets include user data.
@ -169,7 +169,6 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// Optional. /// Optional.
/// </summary> /// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "Fields", Justification = "Imported from ServiceStack")] public IReadOnlyList<ItemFields> Fields { get; set; } = Array.Empty<ItemFields>();
public ItemFields[] Fields { get; set; } = Array.Empty<ItemFields>();
} }
} }

View file

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System;
using System.Collections.Generic;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
@ -17,8 +18,6 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
/// <summary> /// <summary>
/// Gets or sets the device play protocols. /// Gets or sets the device play protocols.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")] public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; } = Array.Empty<MediaProtocol>();
[SuppressMessage("Microsoft.Performance", "SA1011:ClosingBracketsSpace", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
public MediaProtocol[]? DirectPlayProtocols { get; set; }
} }
} }

View file

@ -196,7 +196,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// Start kill timer. /// Start kill timer.
/// </summary> /// </summary>
/// <param name="callback">Callback action.</param> /// <param name="callback">Callback action.</param>
public void StartKillTimer(Action<object> callback) public void StartKillTimer(Action<object?> callback)
{ {
StartKillTimer(callback, PingTimeout); StartKillTimer(callback, PingTimeout);
} }
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// </summary> /// </summary>
/// <param name="callback">Callback action.</param> /// <param name="callback">Callback action.</param>
/// <param name="intervalMs">Callback interval.</param> /// <param name="intervalMs">Callback interval.</param>
public void StartKillTimer(Action<object> callback, int intervalMs) public void StartKillTimer(Action<object?> callback, int intervalMs)
{ {
if (HasExited) if (HasExited)
{ {

View file

@ -101,7 +101,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
return _config.GetConfiguration<EncodingOptions>("encoding"); return _config.GetConfiguration<EncodingOptions>("encoding");
} }
private async void TimerCallback(object state) private async void TimerCallback(object? state)
{ {
if (_job.HasExited) if (_job.HasExited)
{ {

View file

@ -56,7 +56,7 @@ namespace Jellyfin.Api.WebSocketListeners
base.Dispose(dispose); base.Dispose(dispose);
} }
private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{ {
SendData(true); SendData(true);
} }

View file

@ -64,19 +64,19 @@ namespace Jellyfin.Api.WebSocketListeners
base.Dispose(dispose); base.Dispose(dispose);
} }
private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
{ {
SendData(true); SendData(true);
e.Task.TaskProgress -= OnTaskProgress; e.Task.TaskProgress -= OnTaskProgress;
} }
private void OnTaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e) private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
{ {
SendData(true); SendData(true);
e.Argument.TaskProgress += OnTaskProgress; e.Argument.TaskProgress += OnTaskProgress;
} }
private void OnTaskProgress(object sender, GenericEventArgs<double> e) private void OnTaskProgress(object? sender, GenericEventArgs<double> e)
{ {
SendData(false); SendData(false);
} }

View file

@ -66,37 +66,37 @@ namespace Jellyfin.Api.WebSocketListeners
base.Dispose(dispose); base.Dispose(dispose);
} }
private async void OnSessionManagerSessionActivity(object sender, SessionEventArgs e) private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
{ {
await SendData(false).ConfigureAwait(false); await SendData(false).ConfigureAwait(false);
} }
private async void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e) private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
{ {
await SendData(true).ConfigureAwait(false); await SendData(true).ConfigureAwait(false);
} }
private async void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e) private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{ {
await SendData(!e.IsAutomated).ConfigureAwait(false); await SendData(!e.IsAutomated).ConfigureAwait(false);
} }
private async void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
{ {
await SendData(true).ConfigureAwait(false); await SendData(true).ConfigureAwait(false);
} }
private async void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e) private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
{ {
await SendData(true).ConfigureAwait(false); await SendData(true).ConfigureAwait(false);
} }
private async void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
{ {
await SendData(true).ConfigureAwait(false); await SendData(true).ConfigureAwait(false);
} }
private async void OnSessionManagerSessionStarted(object sender, SessionEventArgs e) private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
{ {
await SendData(true).ConfigureAwait(false); await SendData(true).ConfigureAwait(false);
} }

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@ -41,8 +41,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.9" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

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

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using BlurHashSharp.SkiaSharp; using BlurHashSharp.SkiaSharp;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
@ -227,8 +228,8 @@ namespace Jellyfin.Drawing.Skia
} }
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
Directory.CreateDirectory(Path.GetDirectoryName(tempPath)); Directory.CreateDirectory(directory);
File.Copy(path, tempPath, true); File.Copy(path, tempPath, true);
return tempPath; return tempPath;
@ -493,7 +494,8 @@ namespace Jellyfin.Drawing.Skia
// If all we're doing is resizing then we can stop now // If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
Directory.CreateDirectory(outputDirectory);
using var outputStream = new SKFileWStream(outputPath); using var outputStream = new SKFileWStream(outputPath);
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()); using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality); resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
@ -540,7 +542,8 @@ namespace Jellyfin.Drawing.Skia
DrawIndicator(canvas, width, height, options); DrawIndicator(canvas, width, height, options);
} }
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
Directory.CreateDirectory(directory);
using (var outputStream = new SKFileWStream(outputPath)) using (var outputStream = new SKFileWStream(outputPath))
{ {
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@ -24,12 +24,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Linq.Async" Version="4.1.1" /> <PackageReference Include="System.Linq.Async" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.9"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -57,7 +57,8 @@ namespace Jellyfin.Server.Implementations.Users
SerializablePasswordReset spr; SerializablePasswordReset spr;
await using (var str = File.OpenRead(resetFile)) await using (var str = File.OpenRead(resetFile))
{ {
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false); spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
} }
if (spr.ExpirationDate < DateTime.UtcNow) if (spr.ExpirationDate < DateTime.UtcNow)

View file

@ -26,22 +26,22 @@ namespace Jellyfin.Server.Filters
if (attribute is ProducesFileAttribute producesFileAttribute) if (attribute is ProducesFileAttribute producesFileAttribute)
{ {
// Get operation response values. // Get operation response values.
var (_, value) = operation.Responses var response = operation.Responses
.FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal)); .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
// Operation doesn't have a response. // Operation doesn't have a response.
if (value == null) if (response.Value == null)
{ {
continue; continue;
} }
// Clear existing responses. // Clear existing responses.
value.Content.Clear(); response.Value.Content.Clear();
// Add all content-types as file. // Add all content-types as file.
foreach (var contentType in producesFileAttribute.GetContentTypes()) foreach (var contentType in producesFileAttribute.GetContentTypes())
{ {
value.Content.Add(contentType, _openApiMediaType); response.Value.Content.Add(contentType, _openApiMediaType);
} }
break; break;

View file

@ -30,7 +30,8 @@ namespace Jellyfin.Server.Formatters
/// <returns>Write stream task.</returns> /// <returns>Write stream task.</returns>
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{ {
return context.HttpContext.Response.WriteAsync(context.Object?.ToString()); var stringResponse = context.Object?.ToString();
return stringResponse == null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
} }
} }
} }

View file

@ -26,7 +26,8 @@ namespace Jellyfin.Server.Formatters
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{ {
return context.HttpContext.Response.WriteAsync(context.Object?.ToString()); var stringResponse = context.Object?.ToString();
return stringResponse == null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
} }
} }
} }

View file

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<AssemblyName>jellyfin</AssemblyName> <AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@ -38,10 +38,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="prometheus-net" Version="4.0.0" /> <PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

View file

@ -9,6 +9,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty; using SQLitePCL.pretty;
@ -26,6 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly IServerApplicationPaths _paths; private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider; private readonly JellyfinDbProvider _provider;
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
private readonly IUserManager _userManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class. /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
@ -33,11 +35,17 @@ namespace Jellyfin.Server.Migrations.Routines
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param> /// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param> /// <param name="provider">The database provider.</param>
public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider) /// <param name="userManager">The user manager.</param>
public MigrateDisplayPreferencesDb(
ILogger<MigrateDisplayPreferencesDb> logger,
IServerApplicationPaths paths,
JellyfinDbProvider provider,
IUserManager userManager)
{ {
_logger = logger; _logger = logger;
_paths = paths; _paths = paths;
_provider = provider; _provider = provider;
_userManager = userManager;
_jsonOptions = new JsonSerializerOptions(); _jsonOptions = new JsonSerializerOptions();
_jsonOptions.Converters.Add(new JsonStringEnumConverter()); _jsonOptions.Converters.Add(new JsonStringEnumConverter());
} }
@ -86,11 +94,19 @@ namespace Jellyfin.Server.Migrations.Routines
continue; continue;
} }
var dtoUserId = new Guid(result[1].ToBlob());
var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null)
{
_logger.LogWarning("User with ID {UserId} does not exist in the database, skipping migration.", dtoUserId);
continue;
}
var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version) var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
? chromecastDict[version] ? chromecastDict[version]
: ChromecastVersion.Stable; : ChromecastVersion.Stable;
var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString()) var displayPreferences = new DisplayPreferences(dtoUserId, result[2].ToString())
{ {
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null, IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop, ShowBackdrop = dto.ShowBackdrop,

View file

@ -46,6 +46,13 @@ namespace MediaBrowser.Common.Configuration
/// <param name="newConfiguration">The new configuration.</param> /// <param name="newConfiguration">The new configuration.</param>
void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration); void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
/// <summary>
/// Manually pre-loads a factory so that it is available pre system initialisation.
/// </summary>
/// <typeparam name="T">Class to register.</typeparam>
void RegisterConfiguration<T>()
where T : IConfigurationFactory;
/// <summary> /// <summary>
/// Gets the configuration. /// Gets the configuration.
/// </summary> /// </summary>

View file

@ -26,19 +26,40 @@ namespace MediaBrowser.Common.Json.Converters
{ {
if (reader.TokenType == JsonTokenType.String) if (reader.TokenType == JsonTokenType.String)
{ {
var stringEntries = reader.GetString()?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var stringEntries = reader.GetString()?.Split(',', StringSplitOptions.RemoveEmptyEntries);
if (stringEntries == null || stringEntries.Length == 0) if (stringEntries == null || stringEntries.Length == 0)
{ {
return Array.Empty<T>(); return Array.Empty<T>();
} }
var entries = new T[stringEntries.Length]; var parsedValues = new object[stringEntries.Length];
var convertedCount = 0;
for (var i = 0; i < stringEntries.Length; i++) for (var i = 0; i < stringEntries.Length; i++)
{ {
entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim()); try
{
parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
convertedCount++;
}
catch (FormatException)
{
// TODO log when upgraded to .Net5
// _logger.LogWarning(e, "Error converting value.");
}
} }
return entries; var typedValues = new T[convertedCount];
var typedValueIndex = 0;
for (var i = 0; i < stringEntries.Length; i++)
{
if (parsedValues[i] != null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
}
}
return typedValues;
} }
return JsonSerializer.Deserialize<T[]>(ref reader, options); return JsonSerializer.Deserialize<T[]>(ref reader, options);
@ -50,4 +71,4 @@ namespace MediaBrowser.Common.Json.Converters
JsonSerializer.Serialize(writer, value, options); JsonSerializer.Serialize(writer, value, options);
} }
} }
} }

View file

@ -18,8 +18,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup> </ItemGroup>
@ -29,7 +29,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View file

@ -83,16 +83,6 @@ namespace MediaBrowser.Common.Plugins
{ {
} }
/// <inheritdoc />
public virtual void RegisterServices(IServiceCollection serviceCollection)
{
}
/// <inheritdoc />
public virtual void UnregisterServices(IServiceCollection serviceCollection)
{
}
/// <inheritdoc /> /// <inheritdoc />
public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion) public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
{ {
@ -185,6 +175,11 @@ namespace MediaBrowser.Common.Plugins
/// <value>The type of the configuration.</value> /// <value>The type of the configuration.</value>
public Type ConfigurationType => typeof(TConfigurationType); public Type ConfigurationType => typeof(TConfigurationType);
/// <summary>
/// Gets or sets the event handler that is triggered when this configuration changes.
/// </summary>
public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
/// <summary> /// <summary>
/// Gets the name the assembly file. /// Gets the name the assembly file.
/// </summary> /// </summary>
@ -280,6 +275,8 @@ namespace MediaBrowser.Common.Plugins
Configuration = (TConfigurationType)configuration; Configuration = (TConfigurationType)configuration;
SaveConfiguration(); SaveConfiguration();
ConfigurationChanged.Invoke(this, configuration);
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -62,18 +62,6 @@ namespace MediaBrowser.Common.Plugins
/// Called when just before the plugin is uninstalled from the server. /// Called when just before the plugin is uninstalled from the server.
/// </summary> /// </summary>
void OnUninstalling(); void OnUninstalling();
/// <summary>
/// Registers the plugin's services to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
void RegisterServices(IServiceCollection serviceCollection);
/// <summary>
/// Unregisters the plugin's services from the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
void UnregisterServices(IServiceCollection serviceCollection);
} }
public interface IHasPluginConfiguration public interface IHasPluginConfiguration

View file

@ -0,0 +1,19 @@
namespace MediaBrowser.Common.Plugins
{
using Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Defines the <see cref="IPluginServiceRegistrator" />.
/// </summary>
public interface IPluginServiceRegistrator
{
/// <summary>
/// Registers the plugin's services with the service collection.
/// </summary>
/// <remarks>
/// This interface is only used for service registration and requires a parameterless constructor.
/// </remarks>
/// <param name="serviceCollection">The service collection.</param>
void RegisterServices(IServiceCollection serviceCollection);
}
}

View file

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -15,9 +16,9 @@ namespace MediaBrowser.Controller.Dto
ItemFields.RefreshState ItemFields.RefreshState
}; };
public ItemFields[] Fields { get; set; } public IReadOnlyList<ItemFields> Fields { get; set; }
public ImageType[] ImageTypes { get; set; } public IReadOnlyList<ImageType> ImageTypes { get; set; }
public int ImageTypeLimit { get; set; } public int ImageTypeLimit { get; set; }

View file

@ -87,6 +87,8 @@ namespace MediaBrowser.Controller.Entities
public const string InterviewFolderName = "interviews"; public const string InterviewFolderName = "interviews";
public const string SceneFolderName = "scenes"; public const string SceneFolderName = "scenes";
public const string SampleFolderName = "samples"; public const string SampleFolderName = "samples";
public const string ShortsFolderName = "shorts";
public const string FeaturettesFolderName = "featurettes";
public static readonly string[] AllExtrasTypesFolderNames = { public static readonly string[] AllExtrasTypesFolderNames = {
ExtrasFolderName, ExtrasFolderName,
@ -94,7 +96,9 @@ namespace MediaBrowser.Controller.Entities
DeletedScenesFolderName, DeletedScenesFolderName,
InterviewFolderName, InterviewFolderName,
SceneFolderName, SceneFolderName,
SampleFolderName SampleFolderName,
ShortsFolderName,
FeaturettesFolderName
}; };
[JsonIgnore] [JsonIgnore]

View file

@ -225,7 +225,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="fields">The fields.</param> /// <param name="fields">The fields.</param>
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, User user = null); Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
/// <summary> /// <summary>
/// Saves the tuner host. /// Saves the tuner host.

View file

@ -14,8 +14,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup> </ItemGroup>
@ -29,7 +29,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>

View file

@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase)) else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase))
{ {
var rate = part.Split(new[] { '=' }, 2)[^1]; var rate = part.Split('=', 2)[^1];
if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
{ {
@ -103,7 +103,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (state.RunTimeTicks.HasValue && else if (state.RunTimeTicks.HasValue &&
part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
{ {
var time = part.Split(new[] { '=' }, 2).Last(); var time = part.Split('=', 2)[^1];
if (TimeSpan.TryParse(time, _usCulture, out var val)) if (TimeSpan.TryParse(time, _usCulture, out var val))
{ {
@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
{ {
var size = part.Split(new[] { '=' }, 2).Last(); var size = part.Split('=', 2)[^1];
int? scale = null; int? scale = null;
if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1) if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
@ -135,7 +135,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
{ {
var rate = part.Split(new[] { '=' }, 2).Last(); var rate = part.Split('=', 2)[^1];
int? scale = null; int? scale = null;
if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)

View file

@ -486,7 +486,7 @@ namespace MediaBrowser.LocalMetadata.Images
return false; return false;
} }
private FileSystemMetadata GetImage(IEnumerable<FileSystemMetadata> files, string name) private FileSystemMetadata? GetImage(IEnumerable<FileSystemMetadata> files, string name)
{ {
return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0); return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0);
} }

View file

@ -11,7 +11,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View file

@ -683,7 +683,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
default: default:
{ {
string readerName = reader.Name; string readerName = reader.Name;
if (_validProviderIds!.TryGetValue(readerName, out string providerIdValue)) if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
{ {
var id = reader.ReadElementContentAsString(); var id = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(id)) if (!string.IsNullOrWhiteSpace(id))

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