mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-30 19:55:08 -04:00
Merge branch 'master' into xml-parsing-cleanup
This commit is contained in:
commit
1ce49b4a04
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -27,11 +27,11 @@ jobs:
|
|||||||
dotnet-version: '7.0.x'
|
dotnet-version: '7.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
|
uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
|
uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
|
uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
|
||||||
|
2
.github/workflows/repo-stale.yaml
vendored
2
.github/workflows/repo-stale.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
|
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
days-before-stale: 120
|
days-before-stale: 120
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<PackageVersion Include="libse" Version="3.6.13" />
|
<PackageVersion Include="libse" Version="3.6.13" />
|
||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
|
@ -228,7 +228,7 @@ namespace Emby.Dlna
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _fileSystem.GetFilePaths(path)
|
return _fileSystem.GetFilePaths(path)
|
||||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(i => ParseProfileFile(i, type))
|
.Select(i => ParseProfileFile(i, type))
|
||||||
.Where(i => i is not null)
|
.Where(i => i is not null)
|
||||||
.ToList()!; // We just filtered out all the nulls
|
.ToList()!; // We just filtered out all the nulls
|
||||||
|
@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
@ -26,19 +26,18 @@ namespace Emby.Naming.Video
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
|
|
||||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = Path.GetFileNameWithoutExtension(path);
|
var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
|
||||||
var token = Path.GetExtension(path).TrimStart('.');
|
|
||||||
|
|
||||||
foreach (var rule in options.StubTypes)
|
foreach (var rule in options.StubTypes)
|
||||||
{
|
{
|
||||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
stubType = rule.StubType;
|
stubType = rule.StubType;
|
||||||
return true;
|
return true;
|
||||||
|
@ -61,7 +61,7 @@ namespace Emby.Photos
|
|||||||
item.SetImagePath(ImageType.Primary, item.Path);
|
item.SetImagePath(ImageType.Primary, item.Path);
|
||||||
|
|
||||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -101,7 +101,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||||
@ -133,7 +132,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <value>All concrete types.</value>
|
/// <value>All concrete types.</value>
|
||||||
private Type[] _allConcreteTypes;
|
private Type[] _allConcreteTypes;
|
||||||
|
|
||||||
private bool _disposed = false;
|
private bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||||
@ -184,26 +183,16 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
public bool CoreStartupHasCompleted { get; private set; }
|
public bool CoreStartupHasCompleted { get; private set; }
|
||||||
|
|
||||||
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
|
|
||||||
&& !_startupOptions.IsService
|
|
||||||
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public INetworkManager NetManager { get; private set; }
|
public INetworkManager NetManager { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
|
|
||||||
public bool HasPendingRestart { get; private set; }
|
public bool HasPendingRestart { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsShuttingDown { get; private set; }
|
public bool ShouldRestart { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool ShouldRestart { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the logger.
|
/// Gets the logger.
|
||||||
@ -507,6 +496,8 @@ namespace Emby.Server.Implementations
|
|||||||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||||
|
|
||||||
|
serviceCollection.AddScoped<ISystemManager, SystemManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<TmdbClientManager>();
|
serviceCollection.AddSingleton<TmdbClientManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton(NetManager);
|
serviceCollection.AddSingleton(NetManager);
|
||||||
@ -850,24 +841,6 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Restart()
|
|
||||||
{
|
|
||||||
ShouldRestart = true;
|
|
||||||
Shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Shutdown()
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(100).ConfigureAwait(false);
|
|
||||||
IsShuttingDown = true;
|
|
||||||
Resolve<IHostApplicationLifetime>().StopApplication();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the composable part assemblies.
|
/// Gets the composable part assemblies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -923,49 +896,6 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
|
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the system status.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Where this request originated.</param>
|
|
||||||
/// <returns>SystemInfo.</returns>
|
|
||||||
public SystemInfo GetSystemInfo(HttpRequest request)
|
|
||||||
{
|
|
||||||
return new SystemInfo
|
|
||||||
{
|
|
||||||
HasPendingRestart = HasPendingRestart,
|
|
||||||
IsShuttingDown = IsShuttingDown,
|
|
||||||
Version = ApplicationVersionString,
|
|
||||||
WebSocketPortNumber = HttpPort,
|
|
||||||
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
|
|
||||||
Id = SystemId,
|
|
||||||
ProgramDataPath = ApplicationPaths.ProgramDataPath,
|
|
||||||
WebPath = ApplicationPaths.WebPath,
|
|
||||||
LogPath = ApplicationPaths.LogDirectoryPath,
|
|
||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
|
||||||
CachePath = ApplicationPaths.CachePath,
|
|
||||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
|
||||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
|
||||||
ServerName = FriendlyName,
|
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
|
||||||
SupportsLibraryMonitor = true,
|
|
||||||
PackageName = _startupOptions.PackageName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
|
||||||
{
|
|
||||||
return new PublicSystemInfo
|
|
||||||
{
|
|
||||||
Version = ApplicationVersionString,
|
|
||||||
ProductName = ApplicationProductName,
|
|
||||||
Id = SystemId,
|
|
||||||
ServerName = FriendlyName,
|
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
|
||||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(IPAddress remoteAddr)
|
public string GetSmartApiUrl(IPAddress remoteAddr)
|
||||||
{
|
{
|
||||||
|
@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var filePathSpan = filePath.AsSpan();
|
||||||
|
|
||||||
// relative path
|
// relative path
|
||||||
if (firstChar == '\\')
|
if (firstChar == '\\')
|
||||||
{
|
{
|
||||||
filePath = filePath.Substring(1);
|
filePathSpan = filePathSpan.Slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Path.GetFullPath(Path.Combine(folderPath, filePath));
|
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
@ -46,7 +46,6 @@ using MediaBrowser.Model.IO;
|
|||||||
using MediaBrowser.Model.Library;
|
using MediaBrowser.Model.Library;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
|
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
|
||||||
@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
var path = Person.GetPath(name);
|
var path = Person.GetPath(name);
|
||||||
var id = GetItemByNameId<Person>(path);
|
var id = GetItemByNameId<Person>(path);
|
||||||
if (GetItemById(id) is not Person item)
|
if (GetItemById(id) is Person item)
|
||||||
{
|
{
|
||||||
item = new Person
|
return item;
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Id = id,
|
|
||||||
DateCreated = DateTime.UtcNow,
|
|
||||||
DateModified = DateTime.UtcNow,
|
|
||||||
Path = path
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
Name = Path.GetFileName(dir),
|
Name = Path.GetFileName(dir),
|
||||||
|
|
||||||
Locations = _fileSystem.GetFilePaths(dir, false)
|
Locations = _fileSystem.GetFilePaths(dir, false)
|
||||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var saveEntity = false;
|
var saveEntity = false;
|
||||||
var personEntity = GetPerson(person.Name);
|
var personEntity = GetPerson(person.Name);
|
||||||
|
|
||||||
// if PresentationUniqueKey is empty it's likely a new item.
|
if (personEntity is null)
|
||||||
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
|
||||||
{
|
{
|
||||||
|
var path = Person.GetPath(person.Name);
|
||||||
|
personEntity = new Person()
|
||||||
|
{
|
||||||
|
Name = person.Name,
|
||||||
|
Id = GetItemByNameId<Person>(path),
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
Path = path
|
||||||
|
};
|
||||||
|
|
||||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||||
saveEntity = true;
|
saveEntity = true;
|
||||||
}
|
}
|
||||||
@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
||||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(shortcut))
|
if (!string.IsNullOrEmpty(shortcut))
|
||||||
|
@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
|
|
||||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(args.Path);
|
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
|
if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// if audio file exists of same name, return null
|
// if audio file exists of same name, return null
|
||||||
return null;
|
return null;
|
||||||
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
|
|
||||||
if (item is not null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
|
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
item.IsInMixedFolder = true;
|
item.IsInMixedFolder = true;
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
|
return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
return GetBook(args);
|
return GetBook(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(args.Path);
|
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||||
|
|
||||||
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's a book
|
// It's a book
|
||||||
return new Book
|
return new Book
|
||||||
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
{
|
{
|
||||||
var bookFiles = args.FileSystemChildren.Where(f =>
|
var bookFiles = args.FileSystemChildren.Where(f =>
|
||||||
{
|
{
|
||||||
var fileExtension = Path.GetExtension(f.FullName)
|
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
|
||||||
?? string.Empty;
|
|
||||||
|
|
||||||
return _validExtensions.Contains(
|
return _validExtensions.Contains(
|
||||||
fileExtension,
|
fileExtension,
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Don't return a Book if there is more (or less) than one document in the directory
|
// Don't return a Book if there is more (or less) than one document in the directory
|
||||||
|
@ -121,5 +121,7 @@
|
|||||||
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
||||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
||||||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||||
"External": "പുറമേയുള്ള"
|
"External": "പുറമേയുള്ള",
|
||||||
|
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||||
|
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
|
|||||||
{
|
{
|
||||||
var deadImages = images
|
var deadImages = images
|
||||||
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
|
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var image in deadImages)
|
foreach (var image in deadImages)
|
||||||
|
@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
// this is probably best done as a metadata provider
|
// this is probably best done as a metadata provider
|
||||||
// saving a file over itself will require some work to prevent this from happening when not needed
|
// saving a file over itself will require some work to prevent this from happening when not needed
|
||||||
var playlistPath = item.Path;
|
var playlistPath = item.Path;
|
||||||
var extension = Path.GetExtension(playlistPath);
|
var extension = Path.GetExtension(playlistPath.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var playlist = new WplPlaylist();
|
var playlist = new WplPlaylist();
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
string text = new WplContent().ToText(playlist);
|
string text = new WplContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new ZplPlaylist();
|
var playlist = new ZplPlaylist();
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
string text = new ZplContent().ToText(playlist);
|
string text = new ZplContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new M3uPlaylist
|
var playlist = new M3uPlaylist
|
||||||
{
|
{
|
||||||
@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
string text = new M3uContent().ToText(playlist);
|
string text = new M3uContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new M3uPlaylist();
|
var playlist = new M3uPlaylist();
|
||||||
playlist.IsExtended = true;
|
playlist.IsExtended = true;
|
||||||
@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
string text = new M3uContent().ToText(playlist);
|
string text = new M3uContent().ToText(playlist);
|
||||||
File.WriteAllText(playlistPath, text);
|
File.WriteAllText(playlistPath, text);
|
||||||
}
|
}
|
||||||
|
else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
|
||||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var playlist = new PlsPlaylist();
|
var playlist = new PlsPlaylist();
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
|
108
Emby.Server.Implementations/SystemManager.cs
Normal file
108
Emby.Server.Implementations/SystemManager.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Updates;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Model.System;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class SystemManager : ISystemManager
|
||||||
|
{
|
||||||
|
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||||
|
private readonly IServerApplicationHost _applicationHost;
|
||||||
|
private readonly IServerApplicationPaths _applicationPaths;
|
||||||
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
private readonly IStartupOptions _startupOptions;
|
||||||
|
private readonly IInstallationManager _installationManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SystemManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
|
||||||
|
/// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
|
||||||
|
/// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
|
||||||
|
/// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
|
||||||
|
/// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
|
||||||
|
/// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
|
||||||
|
public SystemManager(
|
||||||
|
IHostApplicationLifetime applicationLifetime,
|
||||||
|
IServerApplicationHost applicationHost,
|
||||||
|
IServerApplicationPaths applicationPaths,
|
||||||
|
IServerConfigurationManager configurationManager,
|
||||||
|
IStartupOptions startupOptions,
|
||||||
|
IInstallationManager installationManager)
|
||||||
|
{
|
||||||
|
_applicationLifetime = applicationLifetime;
|
||||||
|
_applicationHost = applicationHost;
|
||||||
|
_applicationPaths = applicationPaths;
|
||||||
|
_configurationManager = configurationManager;
|
||||||
|
_startupOptions = startupOptions;
|
||||||
|
_installationManager = installationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanLaunchWebBrowser => Environment.UserInteractive
|
||||||
|
&& !_startupOptions.IsService
|
||||||
|
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SystemInfo GetSystemInfo(HttpRequest request)
|
||||||
|
{
|
||||||
|
return new SystemInfo
|
||||||
|
{
|
||||||
|
HasPendingRestart = _applicationHost.HasPendingRestart,
|
||||||
|
IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
|
||||||
|
Version = _applicationHost.ApplicationVersionString,
|
||||||
|
WebSocketPortNumber = _applicationHost.HttpPort,
|
||||||
|
CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
|
||||||
|
Id = _applicationHost.SystemId,
|
||||||
|
ProgramDataPath = _applicationPaths.ProgramDataPath,
|
||||||
|
WebPath = _applicationPaths.WebPath,
|
||||||
|
LogPath = _applicationPaths.LogDirectoryPath,
|
||||||
|
ItemsByNamePath = _applicationPaths.InternalMetadataPath,
|
||||||
|
InternalMetadataPath = _applicationPaths.InternalMetadataPath,
|
||||||
|
CachePath = _applicationPaths.CachePath,
|
||||||
|
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||||
|
TranscodingTempPath = _configurationManager.GetTranscodePath(),
|
||||||
|
ServerName = _applicationHost.FriendlyName,
|
||||||
|
LocalAddress = _applicationHost.GetSmartApiUrl(request),
|
||||||
|
SupportsLibraryMonitor = true,
|
||||||
|
PackageName = _startupOptions.PackageName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
||||||
|
{
|
||||||
|
return new PublicSystemInfo
|
||||||
|
{
|
||||||
|
Version = _applicationHost.ApplicationVersionString,
|
||||||
|
ProductName = _applicationHost.Name,
|
||||||
|
Id = _applicationHost.SystemId,
|
||||||
|
ServerName = _applicationHost.FriendlyName,
|
||||||
|
LocalAddress = _applicationHost.GetSmartApiUrl(request),
|
||||||
|
StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Restart() => ShutdownInternal(true);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Shutdown() => ShutdownInternal(false);
|
||||||
|
|
||||||
|
private void ShutdownInternal(bool restart)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(100).ConfigureAwait(false);
|
||||||
|
_applicationHost.ShouldRestart = restart;
|
||||||
|
_applicationLifetime.StopApplication();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
|
|
||||||
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(package.SourceUrl);
|
if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
|
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
|
||||||
return;
|
return;
|
||||||
|
@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
private const string DefaultEventEncoderPreset = "superfast";
|
private const string DefaultEventEncoderPreset = "superfast";
|
||||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||||
|
|
||||||
|
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
|
||||||
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IDlnaManager _dlnaManager;
|
private readonly IDlnaManager _dlnaManager;
|
||||||
@ -1705,16 +1707,28 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||||
|
|
||||||
|
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
|
||||||
|
var strictArgs = string.Empty;
|
||||||
|
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||||
|
if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
|
||||||
|
{
|
||||||
|
strictArgs = " -strict -2";
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.IsOutputVideo)
|
if (!state.IsOutputVideo)
|
||||||
{
|
{
|
||||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
{
|
{
|
||||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
return "-acodec copy" + bitStreamArgs + strictArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioTranscodeParams = string.Empty;
|
var audioTranscodeParams = string.Empty;
|
||||||
|
|
||||||
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
|
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs + strictArgs;
|
||||||
|
|
||||||
var audioBitrate = state.OutputAudioBitrate;
|
var audioBitrate = state.OutputAudioBitrate;
|
||||||
var audioChannels = state.OutputAudioChannels;
|
var audioChannels = state.OutputAudioChannels;
|
||||||
@ -1746,17 +1760,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
return audioTranscodeParams;
|
return audioTranscodeParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
|
||||||
var strictArgs = string.Empty;
|
|
||||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
|
||||||
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
strictArgs = " -strict -2";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
{
|
{
|
||||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||||
@ -2041,9 +2044,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
|
||||||
|
|
||||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
|
||||||
|
|
||||||
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||||
{
|
{
|
||||||
// TODO: Deprecate with new iOS app
|
// TODO: Deprecate with new iOS app
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||||
{
|
{
|
||||||
var file = playlistId + Path.GetExtension(Request.Path);
|
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|
||||||
|
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid segment.");
|
return BadRequest("Invalid segment.");
|
||||||
}
|
}
|
||||||
@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] string segmentId,
|
[FromRoute, Required] string segmentId,
|
||||||
[FromRoute, Required] string segmentContainer)
|
[FromRoute, Required] string segmentContainer)
|
||||||
{
|
{
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||||
|
@ -7,6 +7,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Stream GetFromBase64Stream(Stream inputStream)
|
||||||
|
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the user image.
|
/// Sets the user image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||||
|
|
||||||
await _providerManager
|
await _providerManager
|
||||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||||
|
|
||||||
await _providerManager
|
await _providerManager
|
||||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
return BadRequest("Incorrect ContentType.");
|
return BadRequest("Incorrect ContentType.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
var stream = GetFromBase64Stream(Request.Body);
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
await using (fs.ConfigureAwait(false))
|
await using (fs.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(inputStream);
|
|
||||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
var bytes = Convert.FromBase64String(text);
|
|
||||||
return new MemoryStream(bytes, 0, bytes.Length, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
|
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
|
||||||
{
|
{
|
||||||
int? width = null;
|
int? width = null;
|
||||||
|
@ -6,6 +6,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
|
|||||||
[FromBody, Required] UploadSubtitleDto body)
|
[FromBody, Required] UploadSubtitleDto body)
|
||||||
{
|
{
|
||||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||||
var data = Convert.FromBase64String(body.Data);
|
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||||
var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
|
await using (stream.ConfigureAwait(false))
|
||||||
await using (memoryStream.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
await _subtitleManager.UploadSubtitle(
|
await _subtitleManager.UploadSubtitle(
|
||||||
video,
|
video,
|
||||||
@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
|
|||||||
Language = body.Language,
|
Language = body.Language,
|
||||||
IsForced = body.IsForced,
|
IsForced = body.IsForced,
|
||||||
IsHearingImpaired = body.IsHearingImpaired,
|
IsHearingImpaired = body.IsHearingImpaired,
|
||||||
Stream = memoryStream
|
Stream = stream
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
|
|||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemController : BaseJellyfinApiController
|
public class SystemController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<SystemController> _logger;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly INetworkManager _network;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly ILogger<SystemController> _logger;
|
private readonly ISystemManager _systemManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||||
|
/// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||||
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
|
/// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
/// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
|
||||||
public SystemController(
|
public SystemController(
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
ILogger<SystemController> logger,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
|
IServerApplicationPaths appPaths,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
INetworkManager network,
|
INetworkManager networkManager,
|
||||||
ILogger<SystemController> logger)
|
ISystemManager systemManager)
|
||||||
{
|
{
|
||||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
|
||||||
_appHost = appHost;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_network = network;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_appHost = appHost;
|
||||||
|
_appPaths = appPaths;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_systemManager = systemManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult<SystemInfo> GetSystemInfo()
|
public ActionResult<SystemInfo> GetSystemInfo()
|
||||||
{
|
=> _systemManager.GetSystemInfo(Request);
|
||||||
return _appHost.GetSystemInfo(Request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets public information about the server.
|
/// Gets public information about the server.
|
||||||
@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
[HttpGet("Info/Public")]
|
[HttpGet("Info/Public")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||||
{
|
=> _systemManager.GetPublicSystemInfo(Request);
|
||||||
return _appHost.GetPublicSystemInfo(Request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pings the system.
|
/// Pings the system.
|
||||||
@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
[HttpPost("Ping", Name = "PostPingSystem")]
|
[HttpPost("Ping", Name = "PostPingSystem")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<string> PingSystem()
|
public ActionResult<string> PingSystem()
|
||||||
{
|
=> _appHost.Name;
|
||||||
return _appHost.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Restarts the application.
|
/// Restarts the application.
|
||||||
@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult RestartApplication()
|
public ActionResult RestartApplication()
|
||||||
{
|
{
|
||||||
_appHost.Restart();
|
_systemManager.Restart();
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult ShutdownApplication()
|
public ActionResult ShutdownApplication()
|
||||||
{
|
{
|
||||||
_appHost.Shutdown();
|
_systemManager.Shutdown();
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
return new EndPointInfo
|
return new EndPointInfo
|
||||||
{
|
{
|
||||||
IsLocal = HttpContext.IsLocal(),
|
IsLocal = HttpContext.IsLocal(),
|
||||||
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
||||||
{
|
{
|
||||||
var result = _network.GetMacAddresses()
|
var result = _networkManager.GetMacAddresses()
|
||||||
.Select(i => new WakeOnLanInfo(i));
|
.Select(i => new WakeOnLanInfo(i));
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
@ -200,13 +200,6 @@ public class DynamicHlsHelper
|
|||||||
|
|
||||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||||
{
|
{
|
||||||
// Provide a workaround for the case issue between flac and fLaC.
|
|
||||||
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
|
|
||||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
|
||||||
{
|
|
||||||
builder.Append(flacWaPlaylist);
|
|
||||||
}
|
|
||||||
|
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
||||||
// Provide SDR HEVC entrance for backward compatibility.
|
// Provide SDR HEVC entrance for backward compatibility.
|
||||||
@ -236,14 +229,7 @@ public class DynamicHlsHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||||
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||||
|
|
||||||
// Provide a workaround for the case issue between flac and fLaC.
|
|
||||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
|
|
||||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
|
||||||
{
|
|
||||||
builder.Append(flacWaPlaylist);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore the video codec
|
// Restore the video codec
|
||||||
state.OutputVideoCodec = "copy";
|
state.OutputVideoCodec = "copy";
|
||||||
@ -274,13 +260,6 @@ public class DynamicHlsHelper
|
|||||||
state.VideoStream.Level = originalLevel;
|
state.VideoStream.Level = originalLevel;
|
||||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||||
builder.Append(newPlaylist);
|
builder.Append(newPlaylist);
|
||||||
|
|
||||||
// Provide a workaround for the case issue between flac and fLaC.
|
|
||||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
|
|
||||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
|
||||||
{
|
|
||||||
builder.Append(flacWaPlaylist);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,16 +746,4 @@ public class DynamicHlsHelper
|
|||||||
newValue.ToString(),
|
newValue.ToString(),
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
|
|
||||||
{
|
|
||||||
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ using System.Text;
|
|||||||
namespace Jellyfin.Api.Helpers;
|
namespace Jellyfin.Api.Helpers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hls Codec string helpers.
|
/// Helpers to generate HLS codec strings according to
|
||||||
|
/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
|
||||||
|
/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HlsCodecStringHelpers
|
public static class HlsCodecStringHelpers
|
||||||
{
|
{
|
||||||
@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for FLAC.
|
/// Codec name for FLAC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FLAC = "flac";
|
public const string FLAC = "fLaC";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for ALAC.
|
/// Codec name for ALAC.
|
||||||
@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for OPUS.
|
/// Codec name for OPUS.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string OPUS = "opus";
|
public const string OPUS = "Opus";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a MP3 codec string.
|
/// Gets a MP3 codec string.
|
||||||
|
@ -191,6 +191,11 @@ public static class StreamingHelpers
|
|||||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
containerInternal = ".pcm";
|
||||||
|
}
|
||||||
|
|
||||||
state.OutputAudioCodec = outputAudioCodec;
|
state.OutputAudioCodec = outputAudioCodec;
|
||||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||||
@ -243,7 +248,7 @@ public static class StreamingHelpers
|
|||||||
? GetOutputFileExtension(state, mediaSource)
|
? GetOutputFileExtension(state, mediaSource)
|
||||||
: ("." + state.OutputContainer);
|
: ("." + state.OutputContainer);
|
||||||
|
|
||||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -418,11 +423,11 @@ public static class StreamingHelpers
|
|||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(state.RequestedUrl);
|
var ext = Path.GetExtension(state.RequestedUrl.AsSpan());
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ext))
|
if (ext.IsEmpty)
|
||||||
{
|
{
|
||||||
return ext;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to infer based on the desired video codec
|
// Try to infer based on the desired video codec
|
||||||
@ -504,7 +509,7 @@ public static class StreamingHelpers
|
|||||||
/// <param name="deviceId">The device id.</param>
|
/// <param name="deviceId">The device id.</param>
|
||||||
/// <param name="playSessionId">The play session id.</param>
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
/// <returns>The complete file path, including the folder, for the transcoding file.</returns>
|
/// <returns>The complete file path, including the folder, for the transcoding file.</returns>
|
||||||
private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
|
private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
|
||||||
{
|
{
|
||||||
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
||||||
|
|
||||||
|
@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
|
|||||||
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
|
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
string subtitlePath = state.SubtitleStream.Path;
|
string subtitlePath = state.SubtitleStream.Path;
|
||||||
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
||||||
|
@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authorization.
|
/// Gets the authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpContext">The HTTP context.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
|
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var auth = GetAuthorizationDictionary(httpReq);
|
var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
|
||||||
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
|
|
||||||
|
|
||||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||||
return authInfo;
|
return authInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
auth.TryGetValue("Token", out token);
|
auth.TryGetValue("Token", out token);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
|
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
token = headers["X-Emby-Token"];
|
token = headers["X-Emby-Token"];
|
||||||
@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
// Request doesn't contain a token.
|
// Request doesn't contain a token.
|
||||||
return authInfo;
|
return authInfo;
|
||||||
}
|
}
|
||||||
#pragma warning restore CA1508
|
|
||||||
|
|
||||||
authInfo.HasToken = true;
|
authInfo.HasToken = true;
|
||||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the auth.
|
/// Gets the auth.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpReq">The HTTP request.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
|
||||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
|
|
||||||
{
|
|
||||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(auth))
|
|
||||||
{
|
|
||||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the auth.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
||||||
{
|
{
|
||||||
|
@ -35,21 +35,15 @@ namespace MediaBrowser.Common
|
|||||||
string SystemId { get; }
|
string SystemId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance has pending kernel reload.
|
/// Gets a value indicating whether this instance has pending changes requiring a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
|
||||||
bool HasPendingRestart { get; }
|
bool HasPendingRestart { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance is currently shutting down.
|
/// Gets or sets a value indicating whether the application should restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
|
bool ShouldRestart { get; set; }
|
||||||
bool IsShuttingDown { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the application should restart.
|
|
||||||
/// </summary>
|
|
||||||
bool ShouldRestart { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the application version.
|
/// Gets the application version.
|
||||||
@ -91,11 +85,6 @@ namespace MediaBrowser.Common
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void NotifyPendingRestart();
|
void NotifyPendingRestart();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Restarts this instance.
|
|
||||||
/// </summary>
|
|
||||||
void Restart();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the exports.
|
/// Gets the exports.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -127,11 +116,6 @@ namespace MediaBrowser.Common
|
|||||||
/// <returns>``0.</returns>
|
/// <returns>``0.</returns>
|
||||||
T Resolve<T>();
|
T Resolve<T>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shuts down.
|
|
||||||
/// </summary>
|
|
||||||
void Shutdown();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes this instance.
|
/// Initializes this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
|
|
||||||
string? GetImageCacheTag(User user);
|
string? GetImageCacheTag(User user);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the image.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">The options.</param>
|
|
||||||
/// <param name="toStream">To stream.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task ProcessImage(ImageProcessingOptions options, Stream toStream);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes the image.
|
/// Processes the image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
/// <param name="options">The options.</param>
|
/// <param name="options">The options.</param>
|
||||||
/// <param name="libraryName">The library name to draw onto the collage.</param>
|
/// <param name="libraryName">The library name to draw onto the collage.</param>
|
||||||
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
||||||
|
|
||||||
bool SupportsTransparency(string path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
private bool IsFormatSupported(string originalImagePath)
|
private bool IsFormatSupported(string originalImagePath)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(originalImagePath);
|
var ext = Path.GetExtension(originalImagePath);
|
||||||
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
|
ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller
|
namespace MediaBrowser.Controller
|
||||||
@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
|
|||||||
{
|
{
|
||||||
bool CoreStartupHasCompleted { get; }
|
bool CoreStartupHasCompleted { get; }
|
||||||
|
|
||||||
bool CanLaunchWebBrowser { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the HTTP server port.
|
/// Gets the HTTP server port.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -41,15 +38,6 @@ namespace MediaBrowser.Controller
|
|||||||
/// <value>The name of the friendly.</value>
|
/// <value>The name of the friendly.</value>
|
||||||
string FriendlyName { get; }
|
string FriendlyName { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the system info.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The HTTP request.</param>
|
|
||||||
/// <returns>SystemInfo.</returns>
|
|
||||||
SystemInfo GetSystemInfo(HttpRequest request);
|
|
||||||
|
|
||||||
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a URL specific for the request.
|
/// Gets a URL specific for the request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
34
MediaBrowser.Controller/ISystemManager.cs
Normal file
34
MediaBrowser.Controller/ISystemManager.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using MediaBrowser.Model.System;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A service for managing the application instance.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISystemManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the system info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The HTTP request.</param>
|
||||||
|
/// <returns>The <see cref="SystemInfo"/>.</returns>
|
||||||
|
SystemInfo GetSystemInfo(HttpRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the public system info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The HTTP request.</param>
|
||||||
|
/// <returns>The <see cref="PublicSystemInfo"/>.</returns>
|
||||||
|
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the application restart process.
|
||||||
|
/// </summary>
|
||||||
|
void Restart();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the application shutdown process.
|
||||||
|
/// </summary>
|
||||||
|
void Shutdown();
|
||||||
|
}
|
@ -548,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <returns>System.Nullable{VideoCodecs}.</returns>
|
/// <returns>System.Nullable{VideoCodecs}.</returns>
|
||||||
public string InferVideoCodec(string url)
|
public string InferVideoCodec(string url)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(url);
|
var ext = Path.GetExtension(url.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "wmv";
|
return "wmv";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// TODO: this may not always mean VP8, as the codec ages
|
// TODO: this may not always mean VP8, as the codec ages
|
||||||
return "vp8";
|
return "vp8";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "theora";
|
return "theora";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "h264";
|
return "h264";
|
||||||
}
|
}
|
||||||
@ -1080,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& state.SubtitleStream.IsExternal)
|
&& state.SubtitleStream.IsExternal)
|
||||||
{
|
{
|
||||||
var subtitlePath = state.SubtitleStream.Path;
|
var subtitlePath = state.SubtitleStream.Path;
|
||||||
var subtitleExtension = Path.GetExtension(subtitlePath);
|
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
|
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
|
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||||
if (File.Exists(idxFile))
|
if (File.Exists(idxFile))
|
||||||
@ -6040,7 +6040,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var format = string.Empty;
|
var format = string.Empty;
|
||||||
var keyFrame = string.Empty;
|
var keyFrame = string.Empty;
|
||||||
|
|
||||||
if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase)
|
if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
|
||||||
&& state.BaseRequest.Context == EncodingContext.Streaming)
|
&& state.BaseRequest.Context == EncodingContext.Streaming)
|
||||||
{
|
{
|
||||||
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
||||||
@ -6249,6 +6249,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
|
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
|
||||||
|
audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// opus only supports specific sampling rates
|
// opus only supports specific sampling rates
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace MediaBrowser.MediaEncoding.Attachments
|
namespace MediaBrowser.MediaEncoding.Attachments
|
||||||
{
|
{
|
||||||
public class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
public sealed class AttachmentExtractor : IAttachmentExtractor
|
||||||
{
|
{
|
||||||
private readonly ILogger<AttachmentExtractor> _logger;
|
private readonly ILogger<AttachmentExtractor> _logger;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public AttachmentExtractor(
|
public AttachmentExtractor(
|
||||||
ILogger<AttachmentExtractor> logger,
|
ILogger<AttachmentExtractor> logger,
|
||||||
IApplicationPaths appPaths,
|
IApplicationPaths appPaths,
|
||||||
@ -296,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||||
|
|
||||||
var processArgs = string.Format(
|
var processArgs = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@ -391,33 +388,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefix = filename.Substring(0, 1);
|
var prefix = filename.AsSpan(0, 1);
|
||||||
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
|
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
{
|
{
|
||||||
var files = _fileSystem.GetFilePaths(path, recursive);
|
var files = _fileSystem.GetFilePaths(path, recursive);
|
||||||
|
|
||||||
var excludeExtensions = new[] { ".c" };
|
return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
|
||||||
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@ -652,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||||
|
|
||||||
var outputExtension = targetFormat switch
|
var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
|
||||||
{
|
|
||||||
ImageFormat.Bmp => ".bmp",
|
|
||||||
ImageFormat.Gif => ".gif",
|
|
||||||
ImageFormat.Jpg => ".jpg",
|
|
||||||
ImageFormat.Png => ".png",
|
|
||||||
ImageFormat.Webp => ".webp",
|
|
||||||
_ => ".jpg"
|
|
||||||
};
|
|
||||||
|
|
||||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
||||||
|
@ -24,4 +24,21 @@ public static class ImageFormatExtensions
|
|||||||
ImageFormat.Webp => "image/webp",
|
ImageFormat.Webp => "image/webp",
|
||||||
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the correct extension for this <see cref="ImageFormat" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">This <see cref="ImageFormat" />.</param>
|
||||||
|
/// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
|
||||||
|
/// <returns>The correct extension for this <see cref="ImageFormat" />.</returns>
|
||||||
|
public static string GetExtension(this ImageFormat format)
|
||||||
|
=> format switch
|
||||||
|
{
|
||||||
|
ImageFormat.Bmp => ".bmp",
|
||||||
|
ImageFormat.Gif => ".gif",
|
||||||
|
ImageFormat.Jpg => ".jpg",
|
||||||
|
ImageFormat.Png => ".png",
|
||||||
|
ImageFormat.Webp => ".webp",
|
||||||
|
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||||
fileStreamOptions.Mode = FileMode.Create;
|
fileStreamOptions.Mode = FileMode.Create;
|
||||||
fileStreamOptions.PreallocationSize = source.Length;
|
if (source.CanSeek)
|
||||||
|
{
|
||||||
|
fileStreamOptions.PreallocationSize = source.Length;
|
||||||
|
}
|
||||||
|
|
||||||
var fs = new FileStream(path, fileStreamOptions);
|
var fs = new FileStream(path, fileStreamOptions);
|
||||||
await using (fs.ConfigureAwait(false))
|
await using (fs.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
? Path.GetExtension(attachmentStream.FileName)
|
? Path.GetExtension(attachmentStream.FileName)
|
||||||
: MimeTypes.ToExtension(attachmentStream.MimeType);
|
: MimeTypes.ToExtension(attachmentStream.MimeType);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(extension))
|
|
||||||
{
|
|
||||||
extension = ".jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageFormat format = extension switch
|
ImageFormat format = extension switch
|
||||||
{
|
{
|
||||||
".bmp" => ImageFormat.Bmp,
|
".bmp" => ImageFormat.Bmp,
|
||||||
".gif" => ImageFormat.Gif,
|
".gif" => ImageFormat.Gif,
|
||||||
".jpg" => ImageFormat.Jpg,
|
|
||||||
".png" => ImageFormat.Png,
|
".png" => ImageFormat.Png,
|
||||||
".webp" => ImageFormat.Webp,
|
".webp" => ImageFormat.Webp,
|
||||||
_ => ImageFormat.Jpg
|
_ => ImageFormat.Jpg
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract the last episode number from nfo
|
// Extract the last episode number from nfo
|
||||||
|
// Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
|
||||||
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
||||||
|
var name = new StringBuilder(item.Item.Name);
|
||||||
|
var overview = new StringBuilder(item.Item.Overview);
|
||||||
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
||||||
{
|
{
|
||||||
xml = xmlFile.Substring(0, index + srch.Length);
|
xml = xmlFile.Substring(0, index + srch.Length);
|
||||||
@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
{
|
{
|
||||||
reader.MoveToContent();
|
reader.MoveToContent();
|
||||||
|
|
||||||
if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
|
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||||
{
|
{
|
||||||
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (reader.NodeType == XmlNodeType.Element)
|
||||||
|
{
|
||||||
|
switch (reader.Name)
|
||||||
|
{
|
||||||
|
case "name":
|
||||||
|
case "title":
|
||||||
|
case "localtitle":
|
||||||
|
name.Append(" / ").Append(reader.ReadElementContentAsString());
|
||||||
|
break;
|
||||||
|
case "episode":
|
||||||
|
{
|
||||||
|
if (int.TryParse(reader.ReadElementContentAsString(), out var num))
|
||||||
|
{
|
||||||
|
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "biography":
|
||||||
|
case "plot":
|
||||||
|
case "review":
|
||||||
|
overview.Append(" / ").Append(reader.ReadElementContentAsString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Read();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.Item.Name = name.ToString();
|
||||||
|
item.Item.Overview = overview.ToString();
|
||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
{
|
{
|
||||||
@ -141,6 +177,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case "airsafter_season":
|
case "airsafter_season":
|
||||||
|
case "displayafterseason":
|
||||||
if (reader.TryReadInt(out var airsAfterSeason))
|
if (reader.TryReadInt(out var airsAfterSeason))
|
||||||
{
|
{
|
||||||
item.AirsAfterSeasonNumber = airsAfterSeason;
|
item.AirsAfterSeasonNumber = airsAfterSeason;
|
||||||
|
@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
yield return Path.ChangeExtension(item.Path, ".nfo");
|
|
||||||
|
|
||||||
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
||||||
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
||||||
{
|
{
|
||||||
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield return Path.ChangeExtension(item.Path, ".nfo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
|
var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan())));
|
||||||
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
File.Copy(path, tempPath, true);
|
File.Copy(path, tempPath, true);
|
||||||
@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
{
|
{
|
||||||
if (!orientation.HasValue)
|
if (!orientation.HasValue)
|
||||||
{
|
{
|
||||||
return SKEncodedOrigin.TopLeft;
|
return SKEncodedOrigin.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return orientation.Value switch
|
return (SKEncodedOrigin)orientation.Value;
|
||||||
{
|
|
||||||
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
|
||||||
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
|
||||||
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
|
||||||
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
|
||||||
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
|
||||||
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
|
||||||
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
|
||||||
_ => SKEncodedOrigin.TopLeft
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -38,25 +38,25 @@ public partial class StripCollageBuilder
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(outputPath);
|
ArgumentNullException.ThrowIfNull(outputPath);
|
||||||
|
|
||||||
var ext = Path.GetExtension(outputPath);
|
var ext = Path.GetExtension(outputPath.AsSpan());
|
||||||
|
|
||||||
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
|
if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
|| ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Jpeg;
|
return SKEncodedImageFormat.Jpeg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Webp;
|
return SKEncodedImageFormat.Webp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Gif;
|
return SKEncodedImageFormat.Gif;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
|
if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SKEncodedImageFormat.Bmp;
|
return SKEncodedImageFormat.Bmp;
|
||||||
}
|
}
|
||||||
|
@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
|
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
|
||||||
{
|
|
||||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
|
||||||
using var fileStream = AsyncFile.OpenRead(file.Path);
|
|
||||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
|
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
|
||||||
=> _imageEncoder.SupportedOutputFormats;
|
=> _imageEncoder.SupportedOutputFormats;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool SupportsTransparency(string path)
|
|
||||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
||||||
{
|
{
|
||||||
@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
|
return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
return ImageFormat.Jpg;
|
return ImageFormat.Jpg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMimeType(ImageFormat format, string path)
|
|
||||||
=> format switch
|
|
||||||
{
|
|
||||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
|
||||||
ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
|
|
||||||
ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
|
|
||||||
ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
|
|
||||||
ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
|
|
||||||
_ => MimeTypes.GetMimeType(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache file path based on a set of parameters.
|
/// Gets the cache file path based on a set of parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
filename.Append(",v=");
|
filename.Append(",v=");
|
||||||
filename.Append(Version);
|
filename.Append(Version);
|
||||||
|
|
||||||
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
return Task.FromResult((originalImagePath, dateModified));
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO _mediaEncoder.ConvertImage is not implemented
|
|
||||||
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
//
|
|
||||||
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
|
||||||
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
|
||||||
//
|
|
||||||
// var file = _fileSystem.GetFileInfo(outputPath);
|
|
||||||
// if (!file.Exists)
|
|
||||||
// {
|
|
||||||
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
|
||||||
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// dateModified = file.LastWriteTimeUtc;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// originalImagePath = outputPath;
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return Task.FromResult((originalImagePath, dateModified));
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
|
|||||||
[InlineData((ImageFormat)5)]
|
[InlineData((ImageFormat)5)]
|
||||||
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
||||||
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
|
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetAllImageFormats))]
|
||||||
|
public static void GetExtension_Valid_Valid(ImageFormat format)
|
||||||
|
=> Assert.Null(Record.Exception(() => format.GetExtension()));
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData((ImageFormat)int.MinValue)]
|
||||||
|
[InlineData((ImageFormat)int.MaxValue)]
|
||||||
|
[InlineData((ImageFormat)(-1))]
|
||||||
|
[InlineData((ImageFormat)5)]
|
||||||
|
public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
||||||
|
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension());
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests
|
|||||||
{
|
{
|
||||||
public static class AuthHelper
|
public static class AuthHelper
|
||||||
{
|
{
|
||||||
public const string AuthHeaderName = "X-Emby-Authorization";
|
public const string AuthHeaderName = "Authorization";
|
||||||
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
|
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\"";
|
||||||
|
|
||||||
public static async Task<string> CompleteStartupAsync(HttpClient client)
|
public static async Task<string> CompleteStartupAsync(HttpClient client)
|
||||||
{
|
{
|
||||||
@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests
|
|||||||
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
|
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
|
||||||
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
|
||||||
|
|
||||||
using var content = JsonContent.Create(
|
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName");
|
||||||
|
httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader);
|
||||||
|
httpRequest.Content = JsonContent.Create(
|
||||||
new AuthenticateUserByName()
|
new AuthenticateUserByName()
|
||||||
{
|
{
|
||||||
Username = user!.Name,
|
Username = user!.Name,
|
||||||
Pw = user.Password,
|
Pw = user.Password,
|
||||||
},
|
},
|
||||||
options: jsonOptions);
|
options: jsonOptions);
|
||||||
content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
|
|
||||||
|
|
||||||
using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content);
|
using var authResponse = await client.SendAsync(httpRequest);
|
||||||
|
authResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
|
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
|
||||||
await authResponse.Content.ReadAsStreamAsync(),
|
await authResponse.Content.ReadAsStreamAsync(),
|
||||||
jsonOptions);
|
jsonOptions);
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Integration.Tests.Controllers;
|
||||||
|
|
||||||
|
public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||||
|
{
|
||||||
|
private readonly JellyfinApplicationFactory _factory;
|
||||||
|
private static string? _accessToken;
|
||||||
|
|
||||||
|
public PersonsControllerTests(JellyfinApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPerson_DoesntExist_NotFound()
|
||||||
|
{
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||||
|
|
||||||
|
using var response = await client.GetAsync($"Persons/DoesntExist");
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
|||||||
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
|
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
|
||||||
|
|
||||||
var item = result.Item;
|
var item = result.Item;
|
||||||
Assert.Equal("Rising (1)", item.Name);
|
Assert.Equal("Rising (1) / Rising (2)", item.Name);
|
||||||
Assert.Equal(1, item.IndexNumber);
|
Assert.Equal(1, item.IndexNumber);
|
||||||
Assert.Equal(2, item.IndexNumberEnd);
|
Assert.Equal(2, item.IndexNumberEnd);
|
||||||
Assert.Equal(1, item.ParentIndexNumber);
|
Assert.Equal(1, item.ParentIndexNumber);
|
||||||
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
|
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview);
|
||||||
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
|
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
|
||||||
Assert.Equal(2004, item.ProductionYear);
|
Assert.Equal(2004, item.ProductionYear);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user