mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Compare commits
35 Commits
v10.7.0-rc
...
v10.7.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2851979d4 | ||
|
|
4b6ff7ffa5 | ||
|
|
153123278b | ||
|
|
25c19f79d4 | ||
|
|
0f139e8857 | ||
|
|
e6cc8d5015 | ||
|
|
ef864e24b9 | ||
|
|
ab054d6239 | ||
|
|
19ff447e51 | ||
|
|
4220808b96 | ||
|
|
eb0621a354 | ||
|
|
621c0b9d15 | ||
|
|
fecab1d549 | ||
|
|
bd89cdf8d2 | ||
|
|
557a091865 | ||
|
|
a1773ce97b | ||
|
|
be7411dc58 | ||
|
|
b2a8fd82d8 | ||
|
|
d53120602c | ||
|
|
da09257d58 | ||
|
|
706ac0fafd | ||
|
|
ebd4328f02 | ||
|
|
39b0d69786 | ||
|
|
9f3cebf493 | ||
|
|
20e985a0d1 | ||
|
|
5dbd6f076c | ||
|
|
19a01ccdf3 | ||
|
|
d816995d27 | ||
|
|
a934477850 | ||
|
|
a7f65bd205 | ||
|
|
8138fc3003 | ||
|
|
46a6cd8d1f | ||
|
|
524df2e45d | ||
|
|
a486cd27a9 | ||
|
|
34053b7259 |
@@ -7,7 +7,7 @@ parameters:
|
||||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 5.0.100
|
||||
default: 5.0.103
|
||||
|
||||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
default: "ubuntu-latest"
|
||||
- name: GeneratorVersion
|
||||
type: string
|
||||
default: "5.0.0-beta2"
|
||||
default: "5.0.1"
|
||||
|
||||
jobs:
|
||||
- job: GenerateApiClients
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
DotNetSdkVersion: 5.0.100
|
||||
DotNetSdkVersion: 5.0.103
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
|
||||
@@ -10,7 +10,7 @@ parameters:
|
||||
default: "tests/**/*Tests.csproj"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 5.0.100
|
||||
default: 5.0.103
|
||||
|
||||
jobs:
|
||||
- job: Test
|
||||
|
||||
@@ -6,7 +6,7 @@ variables:
|
||||
- name: RestoreBuildProjects
|
||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
- name: DotNetSdkVersion
|
||||
value: 5.0.100
|
||||
value: 5.0.103
|
||||
|
||||
pr:
|
||||
autoCancel: true
|
||||
|
||||
@@ -228,7 +228,10 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
if (communicationsServer != null)
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -313,9 +316,12 @@ namespace Emby.Dlna.Main
|
||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
||||
// DLNA will only work over http, so we must reset to http:// : {port}
|
||||
uri.Scheme = "http://";
|
||||
uri.Port = _netConfig.HttpServerPortNumber;
|
||||
if (_appHost.PublishedServerUrl == null)
|
||||
{
|
||||
// DLNA will only work over http, so we must reset to http:// : {port}.
|
||||
uri.Scheme = "http";
|
||||
uri.Port = _netConfig.HttpServerPortNumber;
|
||||
}
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
|
||||
@@ -134,6 +134,9 @@ namespace Emby.Server.Implementations
|
||||
|
||||
public bool CoreStartupHasCompleted { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl;
|
||||
|
||||
public virtual bool CanLaunchWebBrowser
|
||||
{
|
||||
get
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
var name = _localizationManager.GetLocalizedString("Collections");
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
||||
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
||||
|
||||
return FindFolders(path).First();
|
||||
}
|
||||
|
||||
@@ -1157,7 +1157,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (episodeSeries != null)
|
||||
{
|
||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
{
|
||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||
}
|
||||
@@ -1207,7 +1207,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (series != null)
|
||||
{
|
||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
{
|
||||
AttachPrimaryImageAspectRatio(dto, series);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -29,7 +31,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// <summary>
|
||||
/// The UDP server.
|
||||
/// </summary>
|
||||
private UdpServer _udpServer;
|
||||
private UdpServer? _udpServer;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed = false;
|
||||
|
||||
@@ -71,9 +73,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
_udpServer.Dispose();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
_udpServer?.Dispose();
|
||||
_udpServer = null;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
@@ -684,7 +684,9 @@ namespace Emby.Server.Implementations.IO
|
||||
return new EnumerationOptions
|
||||
{
|
||||
RecurseSubdirectories = recursive,
|
||||
IgnoreInaccessible = true
|
||||
IgnoreInaccessible = true,
|
||||
// Don't skip any files.
|
||||
AttributesToSkip = 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1240,11 +1240,20 @@ namespace Emby.Server.Implementations.Library
|
||||
return info;
|
||||
}
|
||||
|
||||
private string GetCollectionType(string path)
|
||||
private CollectionTypeOptions? GetCollectionType(string path)
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
|
||||
.Select(Path.GetFileNameWithoutExtension)
|
||||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
||||
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
|
||||
foreach (var file in files)
|
||||
{
|
||||
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
|
||||
// https://github.com/dotnet/runtime/issues/20008
|
||||
if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res))
|
||||
{
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2956,7 +2965,7 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
|
||||
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
@@ -2990,9 +2999,9 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
Directory.CreateDirectory(virtualFolderPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(collectionType))
|
||||
if (collectionType != null)
|
||||
{
|
||||
var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
|
||||
var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection");
|
||||
|
||||
File.WriteAllBytes(path, Array.Empty<byte>());
|
||||
}
|
||||
|
||||
@@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return new MusicArtist();
|
||||
}
|
||||
|
||||
if (_config.Configuration.EnableSimpleArtistDetection)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid mis-identifying top folders
|
||||
if (args.Parent.IsRoot)
|
||||
{
|
||||
|
||||
@@ -2608,7 +2608,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
Locations = new string[] { customPath },
|
||||
Name = "Recorded Movies",
|
||||
CollectionType = CollectionType.Movies
|
||||
CollectionType = CollectionTypeOptions.Movies
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2619,7 +2619,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
Locations = new string[] { customPath },
|
||||
Name = "Recorded Shows",
|
||||
CollectionType = CollectionType.TvShows
|
||||
CollectionType = CollectionTypeOptions.TvShows
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -11,9 +13,11 @@ using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -33,6 +37,21 @@ namespace Emby.Server.Implementations.Plugins
|
||||
private readonly IList<LocalPlugin> _plugins;
|
||||
private readonly Version _minimumVersion;
|
||||
|
||||
private IHttpClientFactory? _httpClientFactory;
|
||||
|
||||
private IHttpClientFactory HttpClientFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_httpClientFactory == null)
|
||||
{
|
||||
_httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
|
||||
}
|
||||
|
||||
return _httpClientFactory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
||||
/// </summary>
|
||||
@@ -112,8 +131,6 @@ namespace Emby.Server.Implementations.Plugins
|
||||
{
|
||||
assembly = Assembly.LoadFrom(file);
|
||||
|
||||
// This force loads all reference dll's that the plugin uses in the try..catch block.
|
||||
// Removing this will cause JF to bomb out if referenced dll's cause issues.
|
||||
assembly.GetExportedTypes();
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
@@ -122,6 +139,20 @@ namespace Emby.Server.Implementations.Plugins
|
||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||
continue;
|
||||
}
|
||||
catch (TypeLoadException ex) // Undocumented exception
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
|
||||
ChangePluginState(plugin, PluginStatus.NotSupported);
|
||||
continue;
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
|
||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
|
||||
yield return assembly;
|
||||
@@ -320,32 +351,74 @@ namespace Emby.Server.Implementations.Plugins
|
||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the manifest back to disk.
|
||||
/// </summary>
|
||||
/// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
|
||||
/// <param name="path">The path where to save the manifest.</param>
|
||||
/// <returns>True if successful.</returns>
|
||||
/// <inheritdoc/>
|
||||
public bool SaveManifest(PluginManifest manifest, string path)
|
||||
{
|
||||
if (manifest == null)
|
||||
try
|
||||
{
|
||||
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
||||
File.WriteAllText(Path.Combine(path, "meta.json"), data);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
|
||||
{
|
||||
if (packageInfo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
|
||||
var imagePath = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
|
||||
{
|
||||
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
||||
File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
|
||||
return true;
|
||||
var url = new Uri(packageInfo.ImageUrl);
|
||||
imagePath = Path.Join(path, url.Segments[^1]);
|
||||
|
||||
await using var fileStream = File.OpenWrite(imagePath);
|
||||
|
||||
try
|
||||
{
|
||||
await using var downloadStream = await HttpClientFactory
|
||||
.CreateClient(NamedClient.Default)
|
||||
.GetStreamAsync(url)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
|
||||
imagePath = string.Empty;
|
||||
}
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception e)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
|
||||
var manifest = new PluginManifest
|
||||
{
|
||||
_logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
|
||||
return false;
|
||||
}
|
||||
Category = packageInfo.Category,
|
||||
Changelog = versionInfo.Changelog ?? string.Empty,
|
||||
Description = packageInfo.Description,
|
||||
Id = new Guid(packageInfo.Id),
|
||||
Name = packageInfo.Name,
|
||||
Overview = packageInfo.Overview,
|
||||
Owner = packageInfo.Owner,
|
||||
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp),
|
||||
Version = versionInfo.Version,
|
||||
Status = PluginStatus.Active,
|
||||
AutoUpdate = true,
|
||||
ImagePath = imagePath
|
||||
};
|
||||
|
||||
return SaveManifest(manifest, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -398,7 +471,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
if (plugin == null)
|
||||
{
|
||||
// Create a dummy record for the providers.
|
||||
// TODO: remove this code, if all provided have been released as separate plugins.
|
||||
// TODO: remove this code once all provided have been released as separate plugins.
|
||||
plugin = new LocalPlugin(
|
||||
instance.AssemblyFilePath,
|
||||
true,
|
||||
|
||||
@@ -1456,7 +1456,12 @@ namespace Emby.Server.Implementations.Session
|
||||
throw new SecurityException("Unknown quick connect token");
|
||||
}
|
||||
|
||||
request.UserId = result.Items[0].UserId;
|
||||
var info = result.Items[0];
|
||||
request.UserId = info.UserId;
|
||||
|
||||
// There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
|
||||
_authRepo.Delete(info);
|
||||
|
||||
return AuthenticateNewSessionInternal(request, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,17 +192,12 @@ namespace Emby.Server.Implementations.Updates
|
||||
var version = package.Versions[i];
|
||||
|
||||
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
|
||||
// Update the manifests, if anything changes.
|
||||
if (plugin != null)
|
||||
{
|
||||
if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
|
||||
{
|
||||
plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
|
||||
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
|
||||
}
|
||||
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path);
|
||||
}
|
||||
|
||||
// Remove versions with a target abi that is greater then the current application version.
|
||||
// Remove versions with a target ABI greater then the current application version.
|
||||
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
|
||||
{
|
||||
package.Versions.RemoveAt(i);
|
||||
@@ -294,7 +289,8 @@ namespace Emby.Server.Implementations.Updates
|
||||
Name = package.Name,
|
||||
Version = v.VersionNumber,
|
||||
SourceUrl = v.SourceUrl,
|
||||
Checksum = v.Checksum
|
||||
Checksum = v.Checksum,
|
||||
PackageInfo = package
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -571,24 +567,16 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
stream.Position = 0;
|
||||
_zipClient.ExtractAllFromZip(stream, targetDir, true);
|
||||
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
|
||||
_pluginManager.ImportPluginFrom(targetDir);
|
||||
}
|
||||
|
||||
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
|
||||
{
|
||||
// Set last update time if we were installed before
|
||||
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
|
||||
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
|
||||
if (plugin != null)
|
||||
{
|
||||
plugin.Manifest.Timestamp = DateTime.UtcNow;
|
||||
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
|
||||
}
|
||||
|
||||
// Do the install
|
||||
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Do plugin-specific processing
|
||||
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
|
||||
|
||||
return plugin != null;
|
||||
|
||||
28
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
Normal file
28
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal produces image attribute.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class AcceptsFileAttribute : Attribute
|
||||
{
|
||||
private readonly string[] _contentTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||
public AcceptsFileAttribute(params string[] contentTypes)
|
||||
{
|
||||
_contentTypes = contentTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured content types.
|
||||
/// </summary>
|
||||
/// <returns>the configured content types.</returns>
|
||||
public string[] GetContentTypes() => _contentTypes;
|
||||
}
|
||||
}
|
||||
18
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Produces file attribute of "image/*".
|
||||
/// </summary>
|
||||
public class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||
{
|
||||
private const string ContentType = "image/*";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
|
||||
/// </summary>
|
||||
public AcceptsImageFileAttribute()
|
||||
: base(ContentType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -121,9 +120,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Users/{userId}/Images/{imageType}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[AcceptsImageFile]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[AcceptsImageFile]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||
@@ -312,6 +314,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[AcceptsImageFile]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
@@ -346,6 +349,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[AcceptsImageFile]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
@@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
@@ -608,7 +608,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
@@ -617,7 +617,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddVirtualFolder(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? collectionType,
|
||||
[FromQuery] CollectionTypeOptions? collectionType,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
|
||||
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
|
||||
@@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool? enableImages,
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Repositories")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
|
||||
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
||||
{
|
||||
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
||||
_serverConfigurationManager.SaveConfiguration();
|
||||
|
||||
@@ -300,9 +300,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
|
||||
if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
|
||||
|| plugin.Manifest.ImagePath == null
|
||||
|| !System.IO.File.Exists(imagePath))
|
||||
if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -144,7 +145,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
|
||||
@@ -509,14 +509,14 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Redeems a forgot password pin.
|
||||
/// </summary>
|
||||
/// <param name="pin">The pin.</param>
|
||||
/// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
|
||||
/// <response code="200">Pin reset process started.</response>
|
||||
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
|
||||
[HttpPost("ForgotPassword/Pin")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
|
||||
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
|
||||
{
|
||||
var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
||||
var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -70,13 +71,13 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetYears(
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
|
||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Helpers
|
||||
if (framerate.HasValue)
|
||||
{
|
||||
builder.Append(",FRAME-RATE=")
|
||||
.Append(framerate.Value);
|
||||
.Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
@@ -8,7 +9,6 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -25,35 +25,27 @@ namespace Jellyfin.Api.Helpers
|
||||
/// <param name="sortBy">Sort By. Comma delimited string.</param>
|
||||
/// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
|
||||
/// <returns>Order By.</returns>
|
||||
public static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
|
||||
public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
|
||||
{
|
||||
var val = sortBy;
|
||||
|
||||
if (string.IsNullOrEmpty(val))
|
||||
if (sortBy.Count == 0)
|
||||
{
|
||||
return Array.Empty<ValueTuple<string, SortOrder>>();
|
||||
}
|
||||
|
||||
var vals = val.Split(',');
|
||||
if (string.IsNullOrWhiteSpace(requestedSortOrder))
|
||||
var result = new (string, SortOrder)[sortBy.Count];
|
||||
var i = 0;
|
||||
// Add elements which have a SortOrder specified
|
||||
for (; i < requestedSortOrder.Count; i++)
|
||||
{
|
||||
requestedSortOrder = "Ascending";
|
||||
result[i] = (sortBy[i], requestedSortOrder[i]);
|
||||
}
|
||||
|
||||
var sortOrders = requestedSortOrder.Split(',');
|
||||
|
||||
var result = new ValueTuple<string, SortOrder>[vals.Length];
|
||||
|
||||
for (var i = 0; i < vals.Length; i++)
|
||||
// Add remaining elements with the first specified SortOrder
|
||||
// or the default one if no SortOrders are specified
|
||||
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
|
||||
for (; i < sortBy.Count; i++)
|
||||
{
|
||||
var sortOrderIndex = sortOrders.Length > i ? i : 0;
|
||||
|
||||
var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
|
||||
var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
|
||||
? SortOrder.Descending
|
||||
: SortOrder.Ascending;
|
||||
|
||||
result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
|
||||
result[i] = (sortBy[i], order);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -210,6 +210,7 @@ namespace Jellyfin.Api.Helpers
|
||||
&& !state.VideoRequest.MaxHeight.HasValue;
|
||||
|
||||
if (isVideoResolutionNotRequested
|
||||
&& state.VideoStream != null
|
||||
&& state.VideoRequest.VideoBitRate.HasValue
|
||||
&& state.VideoStream.BitRate.HasValue
|
||||
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -106,12 +107,14 @@ namespace Jellyfin.Api.Models.LiveTvDtos
|
||||
/// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
|
||||
/// Optional.
|
||||
/// </summary>
|
||||
public string? SortBy { get; set; }
|
||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<string> SortBy { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets sort Order - Ascending,Descending.
|
||||
/// </summary>
|
||||
public string? SortOrder { get; set; }
|
||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<SortOrder> SortOrder { get; set; } = Array.Empty<SortOrder>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the genres to return guide information for.
|
||||
|
||||
16
Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
Normal file
16
Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Jellyfin.Api.Models.UserDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Forgot Password Pin enter request body DTO.
|
||||
/// </summary>
|
||||
public class ForgotPasswordPinDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the entered pin to have the password reset.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string? Pin { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -691,11 +692,11 @@ namespace Jellyfin.Networking.Manager
|
||||
/// Checks the string to see if it matches any interface names.
|
||||
/// </summary>
|
||||
/// <param name="token">String to check.</param>
|
||||
/// <param name="index">Interface index number.</param>
|
||||
/// <param name="index">Interface index numbers that match.</param>
|
||||
/// <returns><c>true</c> if an interface name matches the token, <c>False</c> otherwise.</returns>
|
||||
private bool IsInterface(string token, out int index)
|
||||
private bool TryGetInterfaces(string token, [NotNullWhen(true)] out List<int>? index)
|
||||
{
|
||||
index = -1;
|
||||
index = null;
|
||||
|
||||
// Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
|
||||
// Null check required here for automated testing.
|
||||
@@ -712,13 +713,13 @@ namespace Jellyfin.Networking.Manager
|
||||
if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase))
|
||||
|| (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture)))
|
||||
{
|
||||
index = interfcIndex;
|
||||
return true;
|
||||
index ??= new List<int>();
|
||||
index.Add(interfcIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return index != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -730,14 +731,14 @@ namespace Jellyfin.Networking.Manager
|
||||
{
|
||||
// Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
|
||||
// Null check required here for automated testing.
|
||||
if (IsInterface(token, out int index))
|
||||
if (TryGetInterfaces(token, out var indices))
|
||||
{
|
||||
_logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token);
|
||||
|
||||
// Replace interface tags with the interface IP's.
|
||||
// Replace all the interface tags with the interface IP's.
|
||||
foreach (IPNetAddress iface in _interfaceAddresses)
|
||||
{
|
||||
if (Math.Abs(iface.Tag) == index
|
||||
if (indices.Contains(Math.Abs(iface.Tag))
|
||||
&& ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
|| (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
|
||||
{
|
||||
@@ -916,11 +917,19 @@ namespace Jellyfin.Networking.Manager
|
||||
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
|
||||
if (config.IgnoreVirtualInterfaces)
|
||||
{
|
||||
var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',');
|
||||
var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
|
||||
Array.Copy(lanAddresses, newList, lanAddresses.Length);
|
||||
Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
|
||||
lanAddresses = newList;
|
||||
// each virtual interface name must be pre-pended with the exclusion symbol !
|
||||
var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray();
|
||||
if (lanAddresses.Length > 0)
|
||||
{
|
||||
var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
|
||||
Array.Copy(lanAddresses, newList, lanAddresses.Length);
|
||||
Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
|
||||
lanAddresses = newList;
|
||||
}
|
||||
else
|
||||
{
|
||||
lanAddresses = virtualInterfaceNames;
|
||||
}
|
||||
}
|
||||
|
||||
// Read and parse bind addresses and exclusions, removing ones that don't exist.
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
23
Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs
Normal file
23
Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Jellyfin.Server.Implementations")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
@@ -138,17 +138,14 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
{
|
||||
throw new ArgumentException("Invalid username", nameof(newName));
|
||||
}
|
||||
ThrowIfInvalidUsername(newName);
|
||||
|
||||
if (user.Username.Equals(newName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("The new and old names must be different.");
|
||||
}
|
||||
|
||||
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
|
||||
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new ArgumentException(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@@ -202,9 +199,14 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
/// <inheritdoc/>
|
||||
public async Task<User> CreateUserAsync(string name)
|
||||
{
|
||||
if (!IsValidUsername(name))
|
||||
ThrowIfInvalidUsername(name);
|
||||
|
||||
if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||
throw new ArgumentException(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"A user with the name '{0}' already exists.",
|
||||
name));
|
||||
}
|
||||
|
||||
await using var dbContext = _dbProvider.CreateContext();
|
||||
@@ -726,12 +728,22 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
_users[user.Id] = user;
|
||||
}
|
||||
|
||||
internal static void ThrowIfInvalidUsername(string name)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
|
||||
}
|
||||
|
||||
private static bool IsValidUsername(string name)
|
||||
{
|
||||
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
|
||||
return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
|
||||
return Regex.IsMatch(name, @"^[\w\ \-'._@]+$");
|
||||
}
|
||||
|
||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||
|
||||
@@ -308,11 +308,15 @@ namespace Jellyfin.Server.Extensions
|
||||
?? null;
|
||||
});
|
||||
|
||||
// Allow parameters to properly be nullable.
|
||||
c.UseAllOfToExtendReferenceSchemas();
|
||||
|
||||
// TODO - remove when all types are supported in System.Text.Json
|
||||
c.AddSwaggerTypeMappings();
|
||||
|
||||
c.OperationFilter<SecurityRequirementsOperationFilter>();
|
||||
c.OperationFilter<FileResponseFilter>();
|
||||
c.OperationFilter<FileRequestFilter>();
|
||||
c.OperationFilter<ParameterObsoleteFilter>();
|
||||
c.DocumentFilter<WebsocketModelFilter>();
|
||||
});
|
||||
|
||||
43
Jellyfin.Server/Filters/FileRequestFilter.cs
Normal file
43
Jellyfin.Server/Filters/FileRequestFilter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class FileRequestFilter : IOperationFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata)
|
||||
{
|
||||
if (attribute is AcceptsFileAttribute acceptsFileAttribute)
|
||||
{
|
||||
operation.RequestBody = GetRequestBody(acceptsFileAttribute.GetContentTypes());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenApiRequestBody GetRequestBody(IEnumerable<string> contentTypes)
|
||||
{
|
||||
var body = new OpenApiRequestBody();
|
||||
var mediaType = new OpenApiMediaType
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
};
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
body.Content.Add(contentType, mediaType);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,10 @@
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.2" />
|
||||
<PackageReference Include="prometheus-net" Version="4.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.3" />
|
||||
<PackageReference Include="prometheus-net" Version="4.1.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
|
||||
|
||||
@@ -128,62 +128,63 @@ namespace MediaBrowser.Common.Net
|
||||
/// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
|
||||
public static bool TryParse(string host, out IPHost hostObj)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(host))
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
// See if it's an IPv6 with port address e.g. [::1]:120.
|
||||
int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase);
|
||||
if (i != -1)
|
||||
hostObj = IPHost.None;
|
||||
return false;
|
||||
}
|
||||
|
||||
// See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
|
||||
int i = host.IndexOf("]", StringComparison.OrdinalIgnoreCase);
|
||||
if (i != -1)
|
||||
{
|
||||
return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
|
||||
}
|
||||
|
||||
if (IPNetAddress.TryParse(host, out var netAddress))
|
||||
{
|
||||
// Host name is an ip address, so fake resolve.
|
||||
hostObj = new IPHost(host, netAddress.Address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is it a host, IPv4/6 with/out port?
|
||||
string[] hosts = host.Split(':');
|
||||
|
||||
if (hosts.Length <= 2)
|
||||
{
|
||||
// This is either a hostname: port, or an IP4:port.
|
||||
host = hosts[0];
|
||||
|
||||
if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
// See if it's an IPv6 in [] with no port.
|
||||
i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase);
|
||||
if (i != -1)
|
||||
{
|
||||
return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
|
||||
}
|
||||
|
||||
// Is it a host or IPv4 with port?
|
||||
string[] hosts = host.Split(':');
|
||||
|
||||
if (hosts.Length > 2)
|
||||
{
|
||||
hostObj = new IPHost(string.Empty, IPAddress.None);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove port from IPv4 if it exists.
|
||||
host = hosts[0];
|
||||
|
||||
if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hostObj = new IPHost(host, new IPAddress(Ipv4Loopback));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IPNetAddress.TryParse(host, out IPNetAddress netIP))
|
||||
{
|
||||
// Host name is an ip address, so fake resolve.
|
||||
hostObj = new IPHost(host, netIP.Address);
|
||||
return true;
|
||||
}
|
||||
hostObj = new IPHost(host);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only thing left is to see if it's a host string.
|
||||
if (!string.IsNullOrEmpty(host))
|
||||
if (IPAddress.TryParse(host, out var netIP))
|
||||
{
|
||||
// Use regular expression as CheckHostName isn't RFC5892 compliant.
|
||||
// Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
|
||||
Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
if (re.Match(host).Success)
|
||||
{
|
||||
hostObj = new IPHost(host);
|
||||
return true;
|
||||
}
|
||||
// Host name is an ip address, so fake resolve.
|
||||
hostObj = new IPHost(host, netIP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid host name, as it cannot contain :
|
||||
hostObj = new IPHost(string.Empty, IPAddress.None);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use regular expression as CheckHostName isn't RFC5892 compliant.
|
||||
// Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
|
||||
string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$";
|
||||
|
||||
if (Regex.IsMatch(host, pattern))
|
||||
{
|
||||
hostObj = new IPHost(host);
|
||||
return true;
|
||||
}
|
||||
|
||||
hostObj = IPHost.None;
|
||||
return false;
|
||||
@@ -344,10 +345,14 @@ namespace MediaBrowser.Common.Net
|
||||
{
|
||||
output += "Any Address,";
|
||||
}
|
||||
else
|
||||
else if (i.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
output += $"{i}/32,";
|
||||
}
|
||||
else
|
||||
{
|
||||
output += $"{i}/128,";
|
||||
}
|
||||
}
|
||||
|
||||
output = output[0..^1];
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MediaBrowser.Common.Plugins
|
||||
@@ -44,6 +45,15 @@ namespace MediaBrowser.Common.Plugins
|
||||
/// <returns>True if successful.</returns>
|
||||
bool SaveManifest(PluginManifest manifest, string path);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a manifest from repository data.
|
||||
/// </summary>
|
||||
/// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param>
|
||||
/// <param name="version">Version to be installed.</param>
|
||||
/// <param name="path">The path where to save the manifest.</param>
|
||||
/// <returns>True if successful.</returns>
|
||||
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path);
|
||||
|
||||
/// <summary>
|
||||
/// Imports plugin details from a folder.
|
||||
/// </summary>
|
||||
|
||||
@@ -52,6 +52,11 @@ namespace MediaBrowser.Controller
|
||||
/// <value>The name of the friendly.</value>
|
||||
string FriendlyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured published server url.
|
||||
/// </summary>
|
||||
Uri PublishedServerUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system info.
|
||||
/// </summary>
|
||||
|
||||
@@ -542,7 +542,7 @@ namespace MediaBrowser.Controller.Library
|
||||
|
||||
Guid GetMusicGenreId(string name);
|
||||
|
||||
Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary);
|
||||
Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary);
|
||||
|
||||
Task RemoveVirtualFolder(string name, bool refreshLibrary);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool SupportsHwaccel(string hwaccel);
|
||||
|
||||
/// <summary>
|
||||
/// Whether given filter is supported.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter.</param>
|
||||
/// <param name="option">The option.</param>
|
||||
/// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
|
||||
bool SupportsFilter(string filter, string option);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the audio image.
|
||||
/// </summary>
|
||||
|
||||
@@ -296,6 +296,38 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return found;
|
||||
}
|
||||
|
||||
public bool CheckFilter(string filter, string option)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string output = null;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-h filter=" + filter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting the given filter");
|
||||
}
|
||||
|
||||
if (output.Contains("Filter " + filter, StringComparison.Ordinal))
|
||||
{
|
||||
if (string.IsNullOrEmpty(option))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return output.Contains(option, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Filter: {Name} with option {Option} is not available", filter, option);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetCodecs(Codec codec)
|
||||
{
|
||||
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
|
||||
|
||||
@@ -209,6 +209,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
_ffmpegPath = path;
|
||||
EncoderLocation = location;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -295,6 +296,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool SupportsFilter(string filter, string option)
|
||||
{
|
||||
if (_ffmpegPath != null)
|
||||
{
|
||||
var validator = new EncoderValidator(_logger, _ffmpegPath);
|
||||
return validator.CheckFilter(filter, option);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CanEncodeToAudioCodec(string codec)
|
||||
{
|
||||
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -168,33 +168,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
MediaStream subtitleStream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputFile = mediaSource.Path;
|
||||
var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var protocol = mediaSource.Protocol;
|
||||
if (subtitleStream.IsExternal)
|
||||
{
|
||||
protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
|
||||
}
|
||||
|
||||
var fileInfo = await GetReadableFile(mediaSource.Path, inputFile, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
|
||||
var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (stream, fileInfo.Format);
|
||||
}
|
||||
|
||||
private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
|
||||
private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
if (requiresCharset)
|
||||
if (fileInfo.IsExternal)
|
||||
{
|
||||
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
|
||||
using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var result = CharsetDetector.DetectFromStream(stream).Detected;
|
||||
stream.Position = 0;
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
_logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path);
|
||||
_logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, fileInfo.Path);
|
||||
|
||||
using var reader = new StreamReader(stream, result.Encoding);
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
@@ -204,12 +196,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
|
||||
return File.OpenRead(path);
|
||||
return File.OpenRead(fileInfo.Path);
|
||||
}
|
||||
|
||||
private async Task<SubtitleInfo> GetReadableFile(
|
||||
string mediaPath,
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaStream subtitleStream,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -241,9 +231,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
|
||||
// Extract
|
||||
var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat);
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
|
||||
|
||||
await ExtractTextSubtitle(inputFile, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
|
||||
await ExtractTextSubtitle(mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
|
||||
@@ -255,13 +245,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
if (GetReader(currentFormat, false) == null)
|
||||
{
|
||||
// Convert
|
||||
var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, ".srt");
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
|
||||
|
||||
await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
|
||||
}
|
||||
|
||||
if (subtitleStream.IsExternal)
|
||||
{
|
||||
return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
|
||||
}
|
||||
|
||||
return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
|
||||
}
|
||||
|
||||
@@ -504,7 +499,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// Extracts the text subtitle.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">The input file.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
|
||||
/// <param name="outputCodec">The output codec.</param>
|
||||
@@ -513,7 +507,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
|
||||
private async Task ExtractTextSubtitle(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
int subtitleStreamIndex,
|
||||
string outputCodec,
|
||||
@@ -529,7 +522,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ExtractTextSubtitleInternal(
|
||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||
_mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource),
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
outputPath,
|
||||
@@ -695,15 +688,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSubtitleCachePath(string mediaPath, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
|
||||
private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
|
||||
{
|
||||
if (mediaSource.Protocol == MediaProtocol.File)
|
||||
{
|
||||
var ticksParam = string.Empty;
|
||||
|
||||
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
|
||||
var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path);
|
||||
|
||||
ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
|
||||
ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
|
||||
|
||||
var prefix = filename.Slice(0, 1);
|
||||
|
||||
@@ -711,7 +704,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
|
||||
ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
|
||||
|
||||
var prefix = filename.Slice(0, 1);
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool EnableTonemapping { get; set; }
|
||||
|
||||
public bool EnableVppTonemapping { get; set; }
|
||||
|
||||
public string TonemappingAlgorithm { get; set; }
|
||||
|
||||
public string TonemappingRange { get; set; }
|
||||
@@ -65,6 +67,8 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool EnableDecodingColorDepth10Vp9 { get; set; }
|
||||
|
||||
public bool EnableEnhancedNvdecDecoder { get; set; }
|
||||
|
||||
public bool EnableHardwareEncoding { get; set; }
|
||||
|
||||
public bool AllowHevcEncoding { get; set; }
|
||||
@@ -88,6 +92,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
// The left side of the dot is the platform number, and the right side is the device number on the platform.
|
||||
OpenclDevice = "0.0";
|
||||
EnableTonemapping = false;
|
||||
EnableVppTonemapping = false;
|
||||
TonemappingAlgorithm = "hable";
|
||||
TonemappingRange = "auto";
|
||||
TonemappingDesat = 0;
|
||||
@@ -100,6 +105,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
DeinterlaceMethod = "yadif";
|
||||
EnableDecodingColorDepth10Hevc = true;
|
||||
EnableDecodingColorDepth10Vp9 = true;
|
||||
EnableEnhancedNvdecDecoder = true;
|
||||
EnableHardwareEncoding = true;
|
||||
AllowHevcEncoding = true;
|
||||
EnableSubtitleExtraction = true;
|
||||
|
||||
@@ -254,7 +254,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets the preferred metadata language.
|
||||
/// </summary>
|
||||
/// <value>The preferred metadata language.</value>
|
||||
public string PreferredMetadataLanguage { get; set; } = string.Empty;
|
||||
public string PreferredMetadataLanguage { get; set; } = "en";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata country code.
|
||||
@@ -418,8 +418,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
|
||||
|
||||
public bool EnableSimpleArtistDetection { get; set; } = false;
|
||||
|
||||
public string[] UninstalledPlugins { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
@@ -461,10 +459,5 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
||||
/// </summary>
|
||||
public bool RemoveOldPlugins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether plugin image should be disabled.
|
||||
/// </summary>
|
||||
public bool DisablePluginImages { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
16
MediaBrowser.Model/Entities/CollectionTypeOptions.cs
Normal file
16
MediaBrowser.Model/Entities/CollectionTypeOptions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
public enum CollectionTypeOptions
|
||||
{
|
||||
Movies = 0,
|
||||
TvShows = 1,
|
||||
Music = 2,
|
||||
MusicVideos = 3,
|
||||
HomeVideos = 4,
|
||||
BoxSets = 5,
|
||||
Books = 6,
|
||||
Mixed = 7
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
@@ -9,14 +10,50 @@ namespace MediaBrowser.Model.Entities
|
||||
public static class ProviderIdsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether [has provider identifier] [the specified instance].
|
||||
/// Gets a provider id.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="id">The provider id.</param>
|
||||
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
||||
public static bool TryGetProviderId(this IHasProviderIds instance, string name, [MaybeNullWhen(false)] out string id)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
if (instance.ProviderIds == null)
|
||||
{
|
||||
id = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return instance.ProviderIds.TryGetValue(name, out id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a provider id.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="provider">The provider.</param>
|
||||
/// <returns><c>true</c> if [has provider identifier] [the specified instance]; otherwise, <c>false</c>.</returns>
|
||||
public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
||||
/// <param name="id">The provider id.</param>
|
||||
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
||||
public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [MaybeNullWhen(false)] out string id)
|
||||
{
|
||||
return !string.IsNullOrEmpty(instance.GetProviderId(provider.ToString()));
|
||||
return instance.TryGetProviderId(provider.ToString(), out id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a provider id.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string? GetProviderId(this IHasProviderIds instance, string name)
|
||||
{
|
||||
instance.TryGetProviderId(name, out string? id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,28 +67,6 @@ namespace MediaBrowser.Model.Entities
|
||||
return instance.GetProviderId(provider.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a provider id.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string? GetProviderId(this IHasProviderIds instance, string name)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
if (instance.ProviderIds == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
instance.ProviderIds.TryGetValue(name, out string? id);
|
||||
return string.IsNullOrEmpty(id) ? null : id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a provider id.
|
||||
/// </summary>
|
||||
@@ -68,13 +83,7 @@ namespace MediaBrowser.Model.Entities
|
||||
// If it's null remove the key from the dictionary
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (instance.ProviderIds != null)
|
||||
{
|
||||
if (instance.ProviderIds.ContainsKey(name))
|
||||
{
|
||||
instance.ProviderIds.Remove(name);
|
||||
}
|
||||
}
|
||||
instance.ProviderIds?.Remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace MediaBrowser.Model.Entities
|
||||
/// Gets or sets the type of the collection.
|
||||
/// </summary>
|
||||
/// <value>The type of the collection.</value>
|
||||
public string CollectionType { get; set; }
|
||||
public CollectionTypeOptions? CollectionType { get; set; }
|
||||
|
||||
public LibraryOptions LibraryOptions { get; set; }
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -45,5 +46,11 @@ namespace MediaBrowser.Model.Updates
|
||||
/// </summary>
|
||||
/// <value>The checksum.</value>
|
||||
public string Checksum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets package information for the installation.
|
||||
/// </summary>
|
||||
/// <value>The package information.</value>
|
||||
public PackageInfo PackageInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Find;
|
||||
using TMDbLib.Objects.Search;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@@ -43,63 +45,89 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
|
||||
if (tmdbId == 0)
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var id))
|
||||
{
|
||||
var movieResults = await _tmdbClientManager
|
||||
.SearchMovieAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
|
||||
var movie = await _tmdbClientManager
|
||||
.GetMovieAsync(
|
||||
int.Parse(id, CultureInfo.InvariantCulture),
|
||||
searchInfo.MetadataLanguage,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var remoteSearchResults = new List<RemoteSearchResult>();
|
||||
for (var i = 0; i < movieResults.Count; i++)
|
||||
|
||||
var remoteResult = new RemoteSearchResult
|
||||
{
|
||||
var movieResult = movieResults[i];
|
||||
var remoteSearchResult = new RemoteSearchResult
|
||||
{
|
||||
Name = movieResult.Title ?? movieResult.OriginalTitle,
|
||||
ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath),
|
||||
Overview = movieResult.Overview,
|
||||
SearchProviderName = Name
|
||||
};
|
||||
Name = movie.Title ?? movie.OriginalTitle,
|
||||
SearchProviderName = Name,
|
||||
ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
|
||||
Overview = movie.Overview
|
||||
};
|
||||
|
||||
var releaseDate = movieResult.ReleaseDate?.ToUniversalTime();
|
||||
remoteSearchResult.PremiereDate = releaseDate;
|
||||
remoteSearchResult.ProductionYear = releaseDate?.Year;
|
||||
|
||||
remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture));
|
||||
remoteSearchResults.Add(remoteSearchResult);
|
||||
if (movie.ReleaseDate != null)
|
||||
{
|
||||
var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
|
||||
remoteResult.PremiereDate = releaseDate;
|
||||
remoteResult.ProductionYear = releaseDate.Year;
|
||||
}
|
||||
|
||||
return remoteSearchResults;
|
||||
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(movie.ImdbId))
|
||||
{
|
||||
remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
|
||||
}
|
||||
|
||||
return new[] { remoteResult };
|
||||
}
|
||||
|
||||
var movie = await _tmdbClientManager
|
||||
.GetMovieAsync(tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var remoteResult = new RemoteSearchResult
|
||||
IReadOnlyList<SearchMovie> movieResults;
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id))
|
||||
{
|
||||
Name = movie.Title ?? movie.OriginalTitle,
|
||||
SearchProviderName = Name,
|
||||
ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
|
||||
Overview = movie.Overview
|
||||
};
|
||||
|
||||
if (movie.ReleaseDate != null)
|
||||
var result = await _tmdbClientManager.FindByExternalIdAsync(
|
||||
id,
|
||||
FindExternalSource.Imdb,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
movieResults = result.MovieResults;
|
||||
}
|
||||
else if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id))
|
||||
{
|
||||
var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
|
||||
remoteResult.PremiereDate = releaseDate;
|
||||
remoteResult.ProductionYear = releaseDate.Year;
|
||||
var result = await _tmdbClientManager.FindByExternalIdAsync(
|
||||
id,
|
||||
FindExternalSource.TvDb,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
movieResults = result.MovieResults;
|
||||
}
|
||||
else
|
||||
{
|
||||
movieResults = await _tmdbClientManager
|
||||
.SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(movie.ImdbId))
|
||||
var len = movieResults.Count;
|
||||
var remoteSearchResults = new RemoteSearchResult[len];
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
|
||||
var movieResult = movieResults[i];
|
||||
var remoteSearchResult = new RemoteSearchResult
|
||||
{
|
||||
Name = movieResult.Title ?? movieResult.OriginalTitle,
|
||||
ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath),
|
||||
Overview = movieResult.Overview,
|
||||
SearchProviderName = Name
|
||||
};
|
||||
|
||||
var releaseDate = movieResult.ReleaseDate?.ToUniversalTime();
|
||||
remoteSearchResult.PremiereDate = releaseDate;
|
||||
remoteSearchResult.ProductionYear = releaseDate?.Year;
|
||||
|
||||
remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture));
|
||||
remoteSearchResults[i] = remoteSearchResult;
|
||||
}
|
||||
|
||||
return new[] { remoteResult };
|
||||
return remoteSearchResults;
|
||||
}
|
||||
|
||||
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
|
||||
|
||||
@@ -54,7 +54,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
result.HasMetadata = true;
|
||||
result.Item = new Season
|
||||
{
|
||||
Name = info.Name,
|
||||
IndexNumber = seasonNumber,
|
||||
Overview = seasonResult?.Overview
|
||||
};
|
||||
|
||||
@@ -202,10 +202,11 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
// On Windows, savint the file will fail if the file is hidden or readonly
|
||||
// On Windows, saving the file will fail if the file is hidden or readonly
|
||||
FileSystem.SetAttributes(path, false, false);
|
||||
|
||||
using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
stream.CopyTo(filestream);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.7.0~rc3"
|
||||
version: "10.7.0~rc4"
|
||||
packages:
|
||||
- debian.amd64
|
||||
- debian.arm64
|
||||
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
||||
jellyfin-server (10.7.0~rc4) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.0-rc4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc4
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 21 Feb 2021 13:40:42 -0500
|
||||
|
||||
jellyfin-server (10.7.0~rc3) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.0-rc3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc3
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN apt-get update \
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
%endif
|
||||
|
||||
Name: jellyfin
|
||||
Version: 10.7.0~rc3
|
||||
Version: 10.7.0~rc4
|
||||
Release: 1%{?dist}
|
||||
Summary: The Free Software Media System
|
||||
License: GPLv3
|
||||
@@ -137,6 +137,8 @@ fi
|
||||
%systemd_postun_with_restart jellyfin.service
|
||||
|
||||
%changelog
|
||||
* Sun Feb 21 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.0-rc4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc4
|
||||
* Sat Jan 23 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.0-rc3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc3
|
||||
* Thu Dec 31 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
|
||||
59
tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
Normal file
59
tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Api.Tests.Helpers
|
||||
{
|
||||
public class RequestHelpersTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(GetOrderBy_Success_TestData))]
|
||||
public void GetOrderBy_Success(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder, (string, SortOrder)[] expected)
|
||||
{
|
||||
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetOrderBy_Success_TestData()
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<SortOrder>(),
|
||||
Array.Empty<(string, SortOrder)>()
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
new string[]
|
||||
{
|
||||
"IsFavoriteOrLiked",
|
||||
"Random"
|
||||
},
|
||||
Array.Empty<SortOrder>(),
|
||||
new (string, SortOrder)[]
|
||||
{
|
||||
("IsFavoriteOrLiked", SortOrder.Ascending),
|
||||
("Random", SortOrder.Ascending),
|
||||
}
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
new string[]
|
||||
{
|
||||
"SortName",
|
||||
"ProductionYear"
|
||||
},
|
||||
new SortOrder[]
|
||||
{
|
||||
SortOrder.Descending
|
||||
},
|
||||
new (string, SortOrder)[]
|
||||
{
|
||||
("SortName", SortOrder.Descending),
|
||||
("ProductionYear", SortOrder.Descending),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.14.0" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.2" />
|
||||
<PackageReference Include="AutoFixture" Version="4.15.0" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Model.Tests.Entities
|
||||
{
|
||||
public class ProviderIdsExtensionsTests
|
||||
{
|
||||
private const string ExampleImdbId = "tt0113375";
|
||||
|
||||
[Fact]
|
||||
public void GetProviderId_NullInstance_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.GetProviderId(null!, MetadataProvider.Imdb));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProviderId_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.GetProviderId(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProviderId_NotFoundName_Null()
|
||||
{
|
||||
Assert.Null(ProviderIdsExtensionsTestsObject.Empty.GetProviderId(MetadataProvider.Imdb));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProviderId_NullProvider_Null()
|
||||
{
|
||||
var nullProvider = new ProviderIdsExtensionsTestsObject()
|
||||
{
|
||||
ProviderIds = null!
|
||||
};
|
||||
|
||||
Assert.Null(nullProvider.GetProviderId(MetadataProvider.Imdb));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetProviderId_NotFoundName_False()
|
||||
{
|
||||
Assert.False(ProviderIdsExtensionsTestsObject.Empty.TryGetProviderId(MetadataProvider.Imdb, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetProviderId_NullProvider_False()
|
||||
{
|
||||
var nullProvider = new ProviderIdsExtensionsTestsObject()
|
||||
{
|
||||
ProviderIds = null!
|
||||
};
|
||||
|
||||
Assert.False(nullProvider.TryGetProviderId(MetadataProvider.Imdb, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProviderId_FoundName_Id()
|
||||
{
|
||||
var provider = new ProviderIdsExtensionsTestsObject();
|
||||
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
|
||||
|
||||
Assert.Equal(ExampleImdbId, provider.GetProviderId(MetadataProvider.Imdb));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetProviderId_FoundName_True()
|
||||
{
|
||||
var provider = new ProviderIdsExtensionsTestsObject();
|
||||
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
|
||||
|
||||
Assert.True(provider.TryGetProviderId(MetadataProvider.Imdb, out var id));
|
||||
Assert.Equal(ExampleImdbId, id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProviderId_NullInstance_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.SetProviderId(null!, MetadataProvider.Imdb, ExampleImdbId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProviderId_Null_Remove()
|
||||
{
|
||||
var provider = new ProviderIdsExtensionsTestsObject();
|
||||
provider.SetProviderId(MetadataProvider.Imdb, null!);
|
||||
Assert.Empty(provider.ProviderIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProviderId_EmptyName_Remove()
|
||||
{
|
||||
var provider = new ProviderIdsExtensionsTestsObject();
|
||||
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
|
||||
provider.SetProviderId(MetadataProvider.Imdb, string.Empty);
|
||||
Assert.Empty(provider.ProviderIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProviderId_NonEmptyId_Success()
|
||||
{
|
||||
var provider = new ProviderIdsExtensionsTestsObject();
|
||||
provider.SetProviderId(MetadataProvider.Imdb, ExampleImdbId);
|
||||
Assert.Single(provider.ProviderIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProviderId_NullProvider_Success()
|
||||
{
|
||||
var nullProvider = new ProviderIdsExtensionsTestsObject()
|
||||
{
|
||||
ProviderIds = null!
|
||||
};
|
||||
|
||||
nullProvider.SetProviderId(MetadataProvider.Imdb, ExampleImdbId);
|
||||
Assert.Single(nullProvider.ProviderIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProviderId_NullProviderAndEmptyName_Success()
|
||||
{
|
||||
var nullProvider = new ProviderIdsExtensionsTestsObject()
|
||||
{
|
||||
ProviderIds = null!
|
||||
};
|
||||
|
||||
nullProvider.SetProviderId(MetadataProvider.Imdb, string.Empty);
|
||||
Assert.Null(nullProvider.ProviderIds);
|
||||
}
|
||||
|
||||
private class ProviderIdsExtensionsTestsObject : IHasProviderIds
|
||||
{
|
||||
public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject();
|
||||
|
||||
public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,34 +13,6 @@ namespace Jellyfin.Networking.Tests
|
||||
{
|
||||
public class NetworkParseTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to identify the string and return an object of that class.
|
||||
/// </summary>
|
||||
/// <param name="addr">String to parse.</param>
|
||||
/// <param name="result">IPObject to return.</param>
|
||||
/// <returns>True if the value parsed successfully.</returns>
|
||||
private static bool TryParse(string addr, out IPObject result)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(addr))
|
||||
{
|
||||
// Is it an IP address
|
||||
if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
|
||||
{
|
||||
result = nw;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IPHost.TryParse(addr, out IPHost h))
|
||||
{
|
||||
result = h;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = IPNetAddress.None;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
|
||||
{
|
||||
var configManager = new Mock<IConfigurationManager>
|
||||
@@ -52,15 +24,20 @@ namespace Jellyfin.Networking.Tests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the ability to ignore interfaces
|
||||
/// Checks the ability to ignore virtual interfaces.
|
||||
/// </summary>
|
||||
/// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) | .... </param>
|
||||
/// <param name="lan">LAN addresses.</param>
|
||||
/// <param name="value">Bind addresses that are excluded.</param>
|
||||
[Theory]
|
||||
// All valid
|
||||
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
|
||||
// eth16 only
|
||||
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
|
||||
[InlineData("192.168.1.208/24,-16,vEthernet1|192.168.1.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
|
||||
// All interfaces excluded.
|
||||
[InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[]")]
|
||||
// vEthernet1 and vEthernet212 should be excluded.
|
||||
[InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24]")]
|
||||
public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
|
||||
{
|
||||
var conf = new NetworkConfiguration()
|
||||
@@ -118,11 +95,33 @@ namespace Jellyfin.Networking.Tests
|
||||
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
|
||||
[InlineData("fe80::7add:12ff:febb:c67b%16")]
|
||||
[InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
|
||||
[InlineData("fe80::7add:12ff:febb:c67b%16:123")]
|
||||
[InlineData("[fe80::7add:12ff:febb:c67b%16]")]
|
||||
[InlineData("192.168.1.2/255.255.255.0")]
|
||||
[InlineData("192.168.1.2/24")]
|
||||
public void ValidHostStrings(string address)
|
||||
{
|
||||
Assert.True(IPHost.TryParse(address, out _));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks IP address formats.
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
[Theory]
|
||||
[InlineData("127.0.0.1")]
|
||||
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
|
||||
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
|
||||
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
|
||||
[InlineData("fe80::7add:12ff:febb:c67b%16")]
|
||||
[InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
|
||||
[InlineData("fe80::7add:12ff:febb:c67b%16:123")]
|
||||
[InlineData("[fe80::7add:12ff:febb:c67b%16]")]
|
||||
[InlineData("192.168.1.2/255.255.255.0")]
|
||||
[InlineData("192.168.1.2/24")]
|
||||
public void ValidIPStrings(string address)
|
||||
{
|
||||
Assert.True(TryParse(address, out _));
|
||||
Assert.True(IPNetAddress.TryParse(address, out _));
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +137,8 @@ namespace Jellyfin.Networking.Tests
|
||||
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
|
||||
public void InvalidAddressString(string address)
|
||||
{
|
||||
Assert.False(TryParse(address, out _));
|
||||
Assert.False(IPNetAddress.TryParse(address, out _));
|
||||
Assert.False(IPHost.TryParse(address, out _));
|
||||
}
|
||||
|
||||
|
||||
@@ -172,11 +172,11 @@ namespace Jellyfin.Networking.Tests
|
||||
"[]")]
|
||||
[InlineData(
|
||||
"192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, !10.10.10.10",
|
||||
"[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]",
|
||||
"[192.158.1.2/16,[127.0.0.1/32,::1/128],fd23:184f:2029:0:3139:7386:67d7:d517/128]",
|
||||
"[192.158.1.2/16,127.0.0.1/32]",
|
||||
"[10.10.10.10/32]",
|
||||
"[10.10.10.10/32]",
|
||||
"[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
|
||||
"[192.158.0.0/16,127.0.0.1/32,::1/128,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
|
||||
[InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8",
|
||||
"[192.158.1.2/16,192.169.1.2/8]",
|
||||
"[192.158.1.2/16,192.169.1.2/8]",
|
||||
@@ -333,8 +333,8 @@ namespace Jellyfin.Networking.Tests
|
||||
|
||||
public void TestSubnetContains(string network, string ip)
|
||||
{
|
||||
Assert.True(TryParse(network, out IPObject? networkObj));
|
||||
Assert.True(TryParse(ip, out IPObject? ipObj));
|
||||
Assert.True(IPNetAddress.TryParse(network, out var networkObj));
|
||||
Assert.True(IPNetAddress.TryParse(ip, out var ipObj));
|
||||
Assert.True(networkObj.Contains(ipObj));
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ namespace Jellyfin.Networking.Tests
|
||||
// User on internal network, no binding specified - so result is the 1st internal.
|
||||
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
|
||||
|
||||
// User on external network, internal binding only - so asumption is a proxy forward, return external override.
|
||||
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
|
||||
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
|
||||
|
||||
// User on external network, no binding - so result is the 1st external which is overriden.
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
|
||||
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Users
|
||||
{
|
||||
public class UserManagerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("this_is_valid")]
|
||||
[InlineData("this is also valid")]
|
||||
[InlineData("0@_-' .")]
|
||||
public void ThrowIfInvalidUsername_WhenValidUsername_DoesNotThrowArgumentException(string username)
|
||||
{
|
||||
var ex = Record.Exception(() => UserManager.ThrowIfInvalidUsername(username));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" ")]
|
||||
[InlineData("")]
|
||||
[InlineData("special characters like & $ ? are not allowed")]
|
||||
public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => UserManager.ThrowIfInvalidUsername(username));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user