mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 08:08:16 +00:00
Compare commits
213 Commits
v10.8.0-al
...
v10.8.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4a36d6f7 | ||
|
|
4d1b824651 | ||
|
|
fbc79cb4ce | ||
|
|
4d446eb614 | ||
|
|
de2d292197 | ||
|
|
13ac3e3665 | ||
|
|
5c407dbab6 | ||
|
|
f50b2f7959 | ||
|
|
1acb9e1d45 | ||
|
|
6640f3f782 | ||
|
|
8aef2cb282 | ||
|
|
8bd331f5fa | ||
|
|
ee80602a0d | ||
|
|
03b3f08354 | ||
|
|
65833076db | ||
|
|
e18d966874 | ||
|
|
4cdb590291 | ||
|
|
d47811bdaf | ||
|
|
01a0a4a87c | ||
|
|
87a6fdf847 | ||
|
|
a327b43ab7 | ||
|
|
3f69eeab27 | ||
|
|
dd8b9e9d23 | ||
|
|
3176a4ddd9 | ||
|
|
9cafa2cab4 | ||
|
|
c4c7d7431f | ||
|
|
f2648f3eee | ||
|
|
29095ab390 | ||
|
|
b9b3959223 | ||
|
|
9b71bf8cfe | ||
|
|
5eff80fb8c | ||
|
|
2d77c729f2 | ||
|
|
9d387c542a | ||
|
|
60f4d70cc5 | ||
|
|
bd48b74d5b | ||
|
|
838adaea48 | ||
|
|
46543ead27 | ||
|
|
707e5bab97 | ||
|
|
cf75f99f0e | ||
|
|
fa6f6515a6 | ||
|
|
0e491025ae | ||
|
|
40e05a7993 | ||
|
|
6193fdea69 | ||
|
|
ca2d94ee97 | ||
|
|
99a48554a6 | ||
|
|
120828d8d0 | ||
|
|
9cea773d29 | ||
|
|
a474e4cf3b | ||
|
|
b1025121b8 | ||
|
|
2029076ce8 | ||
|
|
5535b9c01f | ||
|
|
180e2dc329 | ||
|
|
40be86eec0 | ||
|
|
beafd6eaab | ||
|
|
f6d8c19a7a | ||
|
|
2da7777e6d | ||
|
|
a0df79d8a5 | ||
|
|
01b95cf8e6 | ||
|
|
6bbfcf1906 | ||
|
|
7b50048020 | ||
|
|
a9a53dc657 | ||
|
|
0d8170cedb | ||
|
|
1a35690834 | ||
|
|
c61b9ef05a | ||
|
|
b5b994b22f | ||
|
|
0894a6193f | ||
|
|
9d34d6339a | ||
|
|
bbf1399826 | ||
|
|
d016d483ae | ||
|
|
61b191d345 | ||
|
|
9433072f90 | ||
|
|
a3c5afa443 | ||
|
|
f1862f9b1a | ||
|
|
a68e58556c | ||
|
|
5e91f50c43 | ||
|
|
c1a8385c9c | ||
|
|
9978164438 | ||
|
|
eaa003775f | ||
|
|
e778462955 | ||
|
|
c677b4f6b7 | ||
|
|
20b0499b00 | ||
|
|
09f2456919 | ||
|
|
bcd84dedda | ||
|
|
2496c5d589 | ||
|
|
19c9319d2c | ||
|
|
6ef9ac6e53 | ||
|
|
502a5fc932 | ||
|
|
02ce95edaf | ||
|
|
5addb24e9b | ||
|
|
dc27e7618f | ||
|
|
3e1bbdd3d4 | ||
|
|
2ff786fd3d | ||
|
|
2a09d4244c | ||
|
|
beef6f0855 | ||
|
|
80c7119537 | ||
|
|
1df5b5034b | ||
|
|
4a20ae6cb4 | ||
|
|
a87b87825d | ||
|
|
976e3160b8 | ||
|
|
ca887518dd | ||
|
|
5b5ae1ef52 | ||
|
|
3eec137100 | ||
|
|
402a3797a4 | ||
|
|
0b871505a6 | ||
|
|
0485ff1899 | ||
|
|
69df004b9f | ||
|
|
8b9ff893b3 | ||
|
|
b3d5383073 | ||
|
|
9d5f328a09 | ||
|
|
f469ee3010 | ||
|
|
e30ac91f16 | ||
|
|
95183c365a | ||
|
|
0af5e60094 | ||
|
|
3c030740ea | ||
|
|
bb9ce98379 | ||
|
|
02fda265b8 | ||
|
|
501919df35 | ||
|
|
107909de30 | ||
|
|
052b667d6a | ||
|
|
e5ea607853 | ||
|
|
223cc8314a | ||
|
|
39e6658d01 | ||
|
|
8f051c86f7 | ||
|
|
11419cbbfd | ||
|
|
5b7e8a27fb | ||
|
|
718e4b8665 | ||
|
|
d387d4df92 | ||
|
|
1fa845de6e | ||
|
|
7126f9dffc | ||
|
|
b2b4bd82d7 | ||
|
|
03c7bcf9c6 | ||
|
|
ea355b4262 | ||
|
|
71a0abe211 | ||
|
|
d8c3b8e7f8 | ||
|
|
88baff5693 | ||
|
|
6d3b129666 | ||
|
|
8295a3be0d | ||
|
|
17a273d237 | ||
|
|
752bb88445 | ||
|
|
a263f3978b | ||
|
|
0009e5cc27 | ||
|
|
96ea865681 | ||
|
|
4a7498a0be | ||
|
|
e3f0a53f59 | ||
|
|
9ba7bf96ef | ||
|
|
bff5ff0cb8 | ||
|
|
acb86066ff | ||
|
|
263bbf897a | ||
|
|
97124f5fce | ||
|
|
6565b0cfbe | ||
|
|
b635b5a7e3 | ||
|
|
6d74c83ddb | ||
|
|
7cf5767949 | ||
|
|
fa366f0099 | ||
|
|
989013c974 | ||
|
|
c09e999916 | ||
|
|
61b75c82ce | ||
|
|
773a4ae1f3 | ||
|
|
38ac84ad71 | ||
|
|
054ca6a8fb | ||
|
|
3d9f865247 | ||
|
|
5cd6fba11a | ||
|
|
f7b18d97a7 | ||
|
|
45e6e2adee | ||
|
|
704c1539a2 | ||
|
|
00b293d2e9 | ||
|
|
52e9dc66f5 | ||
|
|
3c8f7d380f | ||
|
|
0a0ddb0eaf | ||
|
|
84b8c9c2bc | ||
|
|
257e1be95f | ||
|
|
6003289717 | ||
|
|
19d285149b | ||
|
|
33a2e1bd50 | ||
|
|
b8507371d5 | ||
|
|
bddded96ff | ||
|
|
24024706bf | ||
|
|
93fd1c7075 | ||
|
|
5b1b2621ab | ||
|
|
b50c3852ef | ||
|
|
1924d0740d | ||
|
|
74459ec403 | ||
|
|
c32a421ea7 | ||
|
|
4cfe8fe588 | ||
|
|
06c82973c6 | ||
|
|
f638cd08ea | ||
|
|
31b87130a2 | ||
|
|
ca99a27611 | ||
|
|
5eb1fde88c | ||
|
|
24679af2e8 | ||
|
|
474b035d99 | ||
|
|
4f45c52674 | ||
|
|
f059be8e4d | ||
|
|
03435641c8 | ||
|
|
58be1d7759 | ||
|
|
4e0edaf544 | ||
|
|
9c74103fbe | ||
|
|
551c6f02a2 | ||
|
|
358cf48506 | ||
|
|
331d4ad849 | ||
|
|
bd32cecf7a | ||
|
|
c84f2e48b0 | ||
|
|
370b7f8e12 | ||
|
|
4cb649853d | ||
|
|
a774d1fa10 | ||
|
|
5254e74719 | ||
|
|
14b5e85461 | ||
|
|
25f1cdbcb5 | ||
|
|
34df1a030b | ||
|
|
1d729b2b0f | ||
|
|
f73a7a6ed8 | ||
|
|
de9bf327c6 | ||
|
|
5265b3eee7 |
@@ -150,6 +150,7 @@
|
||||
- [ianjazz246](https://github.com/ianjazz246)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
- [MBR-0001](https://github.com/MBR-0001)
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -395,6 +395,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
}
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.Flush();
|
||||
xmlWriter.WriteElementString("Result", builder.ToString());
|
||||
}
|
||||
|
||||
@@ -484,6 +485,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
}
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.Flush();
|
||||
xmlWriter.WriteElementString("Result", builder.ToString());
|
||||
}
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ namespace Emby.Dlna
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.CreateNew;
|
||||
fileOptions.Mode = FileMode.Create;
|
||||
fileOptions.PreallocationSize = length;
|
||||
using (var fileStream = new FileStream(path, fileOptions))
|
||||
{
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Emby.Dlna.Main
|
||||
config);
|
||||
Current = this;
|
||||
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||
|
||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||
@@ -262,7 +262,6 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
_networkManager,
|
||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||
Environment.OSVersion.VersionString,
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
@@ -400,7 +399,6 @@ namespace Emby.Dlna.Main
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_config,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
|
||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_config = config;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
||||
@@ -19,6 +19,7 @@ using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Emby.Drawing;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Notifications;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Archiving;
|
||||
@@ -312,22 +313,6 @@ namespace Emby.Server.Implementations
|
||||
? Environment.MachineName
|
||||
: ConfigurationManager.Configuration.ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||
/// </summary>
|
||||
private void MigrateNetworkConfiguration()
|
||||
{
|
||||
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var networkSettings = new NetworkConfiguration();
|
||||
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||
Logger.LogDebug("Successfully migrated network settings.");
|
||||
}
|
||||
}
|
||||
|
||||
public string ExpandVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
@@ -512,8 +497,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
|
||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
||||
MigrateNetworkConfiguration();
|
||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
|
||||
// Initialize runtime stat collection
|
||||
@@ -596,6 +579,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
serviceCollection.AddSingleton<NamingOptions>();
|
||||
|
||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
|
||||
@@ -1126,12 +1110,12 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetApiUrlForLocalAccess(bool allowHttps)
|
||||
public string GetApiUrlForLocalAccess(bool allowHttps = true)
|
||||
{
|
||||
// With an empty source, the port will be null
|
||||
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
||||
var scheme = allowHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
||||
var port = allowHttps ? HttpsPort : HttpPort;
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
||||
}
|
||||
|
||||
|
||||
@@ -1075,14 +1075,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
// was used for status
|
||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
||||
// {
|
||||
// item.ExternalEtag = info.Etag;
|
||||
// forceUpdate = true;
|
||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
||||
// }
|
||||
|
||||
if (!internalChannelId.Equals(item.ChannelId))
|
||||
{
|
||||
forceUpdate = true;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using static MediaBrowser.Common.Cryptography.Constants;
|
||||
using static MediaBrowser.Model.Cryptography.Constants;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
@@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
/// </summary>
|
||||
public class CryptographyProvider : ICryptoProvider
|
||||
{
|
||||
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
||||
// Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
||||
// there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
||||
// Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
||||
// TODO: remove when not needed for backwards compat
|
||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||
{
|
||||
"MD5",
|
||||
@@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
public string DefaultHashMethod => "PBKDF2-SHA512";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetSupportedHashMethods()
|
||||
=> _supportedHashMethods;
|
||||
|
||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
|
||||
{
|
||||
// downgrading for now as we need this library to be dotnetstandard compliant
|
||||
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
||||
if (method != DefaultHashMethod)
|
||||
{
|
||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
||||
}
|
||||
|
||||
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
||||
return r.GetBytes(32);
|
||||
byte[] salt = GenerateSalt();
|
||||
return new PasswordHash(
|
||||
DefaultHashMethod,
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
DefaultIterations,
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength),
|
||||
salt,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||
public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
|
||||
{
|
||||
if (hashMethod == DefaultHashMethod)
|
||||
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||
{
|
||||
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
HashAlgorithmName.SHA1,
|
||||
32));
|
||||
}
|
||||
|
||||
if (!_supportedHashMethods.Contains(hashMethod))
|
||||
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||
{
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength));
|
||||
}
|
||||
|
||||
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
|
||||
if (salt.Length == 0)
|
||||
if (!_supportedHashMethods.Contains(hash.Id))
|
||||
{
|
||||
return h.ComputeHash(bytes);
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
|
||||
}
|
||||
|
||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
||||
using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
|
||||
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
|
||||
if (hash.Salt.Length == 0)
|
||||
{
|
||||
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
|
||||
}
|
||||
|
||||
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
|
||||
Array.Copy(bytes, salted, bytes.Length);
|
||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
||||
return h.ComputeHash(salted);
|
||||
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
|
||||
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt()
|
||||
=> GenerateSalt(DefaultSaltLength);
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt(int length)
|
||||
=> RandomNumberGenerator.GetBytes(length);
|
||||
{
|
||||
var salt = new byte[length];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetNonZeroBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class ManagedConnection : IDisposable
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _writeLock;
|
||||
|
||||
|
||||
@@ -370,6 +370,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (item is MusicAlbum || item is Season || item is Playlist)
|
||||
{
|
||||
dto.ChildCount = dto.RecursiveItemCount;
|
||||
var folderChildCount = folder.LinkedChildren.Length;
|
||||
// The default is an empty array, so we can't reliably use the count when it's empty
|
||||
if (folderChildCount > 0)
|
||||
{
|
||||
dto.ChildCount ??= folderChildCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.1" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ILogger<ExternalPortForwarding> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
||||
|
||||
@@ -42,17 +41,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="appHost">The application host.</param>
|
||||
/// <param name="config">The configuration manager.</param>
|
||||
/// <param name="deviceDiscovery">The device discovery.</param>
|
||||
public ExternalPortForwarding(
|
||||
ILogger<ExternalPortForwarding> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IServerConfigurationManager config,
|
||||
IDeviceDiscovery deviceDiscovery)
|
||||
IServerConfigurationManager config)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_config = config;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
}
|
||||
|
||||
private string GetConfigIdentifier()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class FileRefresher : IDisposable
|
||||
public sealed class FileRefresher : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private readonly List<string> _affectedPaths = new List<string>();
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private Timer? _timer;
|
||||
private bool _disposed;
|
||||
|
||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
|
||||
AddPath(path);
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> Completed;
|
||||
public event EventHandler<EventArgs>? Completed;
|
||||
|
||||
public string Path { get; private set; }
|
||||
|
||||
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
private void OnTimerCallback(object state)
|
||||
private void OnTimerCallback(object? state)
|
||||
{
|
||||
List<string> paths;
|
||||
|
||||
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
try
|
||||
{
|
||||
ProcessPathChanges(paths.ToList());
|
||||
ProcessPathChanges(paths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private void ProcessPathChanges(List<string> paths)
|
||||
{
|
||||
var itemsToRefresh = paths
|
||||
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(GetAffectedBaseItem)
|
||||
.Where(item => item != null)
|
||||
.GroupBy(x => x.Id)
|
||||
.Select(x => x.First());
|
||||
.GroupBy(x => x!.Id) // Removed null values in the previous .Where()
|
||||
.Select(x => x.First())!;
|
||||
|
||||
foreach (var item in itemsToRefresh)
|
||||
{
|
||||
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private BaseItem GetAffectedBaseItem(string path)
|
||||
private BaseItem? GetAffectedBaseItem(string path)
|
||||
{
|
||||
BaseItem item = null;
|
||||
BaseItem? item = null;
|
||||
|
||||
while (item == null && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
item = _libraryManager.FindByPath(path, null);
|
||||
|
||||
path = System.IO.Path.GetDirectoryName(path);
|
||||
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
|
||||
@@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
|
||||
newRefresher.Completed += NewRefresher_Completed;
|
||||
newRefresher.Completed += OnNewRefresherCompleted;
|
||||
_activeRefreshers.Add(newRefresher);
|
||||
}
|
||||
}
|
||||
|
||||
private void NewRefresher_Completed(object sender, EventArgs e)
|
||||
private void OnNewRefresherCompleted(object sender, EventArgs e)
|
||||
{
|
||||
var refresher = (FileRefresher)sender;
|
||||
DisposeRefresher(refresher);
|
||||
@@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
lock (_activeRefreshers)
|
||||
{
|
||||
refresher.Completed -= OnNewRefresherCompleted;
|
||||
refresher.Dispose();
|
||||
_activeRefreshers.Remove(refresher);
|
||||
}
|
||||
@@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
foreach (var refresher in _activeRefreshers.ToList())
|
||||
{
|
||||
refresher.Completed -= OnNewRefresherCompleted;
|
||||
refresher.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
@@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library
|
||||
/// </summary>
|
||||
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IServerApplicationPaths _serverApplicationPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="serverApplicationPaths">The server application paths.</param>
|
||||
public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
|
||||
public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
_serverApplicationPaths = serverApplicationPaths;
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
// Don't resolve these into audio files
|
||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||
&& _libraryManager.IsAudioFile(filename))
|
||||
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
@@ -88,9 +89,6 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||
|
||||
private NamingOptions _namingOptions;
|
||||
private string[] _videoFileExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder.
|
||||
/// </summary>
|
||||
@@ -116,6 +114,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILogger<LibraryManager> logger,
|
||||
@@ -130,7 +129,8 @@ namespace Emby.Server.Implementations.Library
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache)
|
||||
IMemoryCache memoryCache,
|
||||
NamingOptions namingOptions)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = logger;
|
||||
@@ -146,6 +146,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_itemRepository = itemRepository;
|
||||
_imageProcessor = imageProcessor;
|
||||
_memoryCache = memoryCache;
|
||||
_namingOptions = namingOptions;
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
@@ -2500,16 +2501,6 @@ namespace Emby.Server.Implementations.Library
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
return VideoResolver.IsVideoFile(path, GetNamingOptions());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAudioFile(string path)
|
||||
=> AudioFileParser.IsAudioFile(path, GetNamingOptions());
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? GetSeasonNumberFromPath(string path)
|
||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||
@@ -2525,7 +2516,7 @@ namespace Emby.Server.Implementations.Library
|
||||
isAbsoluteNaming = null;
|
||||
}
|
||||
|
||||
var resolver = new EpisodeResolver(GetNamingOptions());
|
||||
var resolver = new EpisodeResolver(_namingOptions);
|
||||
|
||||
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
||||
|
||||
@@ -2682,21 +2673,9 @@ namespace Emby.Server.Implementations.Library
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NamingOptions GetNamingOptions()
|
||||
{
|
||||
if (_namingOptions == null)
|
||||
{
|
||||
_namingOptions = new NamingOptions();
|
||||
_videoFileExtensions = _namingOptions.VideoFileExtensions;
|
||||
}
|
||||
|
||||
return _namingOptions;
|
||||
}
|
||||
|
||||
public ItemLookupInfo ParseName(string name)
|
||||
{
|
||||
var namingOptions = GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
var result = VideoResolver.CleanDateTime(name, namingOptions);
|
||||
|
||||
return new ItemLookupInfo
|
||||
@@ -2708,11 +2687,11 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var namingOptions = GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
|
||||
.Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
|
||||
.ToList();
|
||||
|
||||
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||
@@ -2726,7 +2705,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var resolvers = new IItemResolver[]
|
||||
{
|
||||
new GenericVideoResolver<Trailer>(this)
|
||||
new GenericVideoResolver<Trailer>(_namingOptions)
|
||||
};
|
||||
|
||||
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
|
||||
@@ -2752,11 +2731,11 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var namingOptions = GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
|
||||
.Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
|
||||
.ToList();
|
||||
|
||||
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||
@@ -2840,7 +2819,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private void SetExtraTypeFromFilename(Video item)
|
||||
{
|
||||
var resolver = new ExtraResolver(GetNamingOptions());
|
||||
var resolver = new ExtraResolver(_namingOptions);
|
||||
|
||||
var result = resolver.GetExtraInfo(item.Path);
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.AudioBook;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
/// </summary>
|
||||
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
public AudioResolver(ILibraryManager libraryManager)
|
||||
public AudioResolver(NamingOptions namingOptions)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
@@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
string collectionType)
|
||||
{
|
||||
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true);
|
||||
return ResolveMultipleAudio(parent, files, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return null;
|
||||
}
|
||||
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
|
||||
return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
return FindAudioBook(args, false);
|
||||
}
|
||||
|
||||
if (_libraryManager.IsAudioFile(args.Path))
|
||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
|
||||
@@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
|
||||
|
||||
// For conflicting extensions, give priority to videos
|
||||
if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path))
|
||||
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return null;
|
||||
}
|
||||
|
||||
private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
|
||||
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
|
||||
private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
|
||||
{
|
||||
// TODO: Allow GetMultiDiscMovie in here
|
||||
const bool supportsMultiVersion = false;
|
||||
var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName);
|
||||
|
||||
var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ??
|
||||
new MultiItemResolverResult();
|
||||
|
||||
if (result.Items.Count == 1)
|
||||
if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
|
||||
{
|
||||
// If we were supporting this we'd be checking filesFromOtherItems
|
||||
var item = (T)result.Items[0];
|
||||
item.IsInMixedFolder = false;
|
||||
item.Name = Path.GetFileName(item.ContainingFolderPath);
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
// If we were supporting this we'd be checking filesFromOtherItems
|
||||
item.IsInMixedFolder = false;
|
||||
item.Name = Path.GetFileName(item.ContainingFolderPath);
|
||||
return item;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName)
|
||||
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
|
||||
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var items = new List<BaseItem>();
|
||||
@@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnored(child.Name))
|
||||
else
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
|
||||
var resolver = new AudioBookListResolver(namingOptions);
|
||||
var resolver = new AudioBookListResolver(_namingOptions);
|
||||
var resolverResult = resolver.Resolve(files).ToList();
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
@@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
var firstMedia = resolvedItem.Files[0];
|
||||
|
||||
var libraryItem = new T
|
||||
var libraryItem = new AudioBook
|
||||
{
|
||||
Path = firstMedia.Path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
@@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file)
|
||||
private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
|
||||
private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
||||
@@ -246,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
/// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
|
||||
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
|
||||
{
|
||||
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
|
||||
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// if (args.Parent is MusicArtist) return true; // saves us from testing children twice
|
||||
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
|
||||
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -116,13 +114,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
private bool ContainsMusic(
|
||||
IEnumerable<FileSystemMetadata> list,
|
||||
bool allowSubfolders,
|
||||
IDirectoryService directoryService,
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager)
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
// check for audio files before digging down into directories
|
||||
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
|
||||
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
|
||||
if (foundAudioFile)
|
||||
{
|
||||
// at least one audio file exists
|
||||
@@ -137,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
var discSubfolderCount = 0;
|
||||
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
var parser = new AlbumParser(namingOptions);
|
||||
var parser = new AlbumParser(_namingOptions);
|
||||
|
||||
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
|
||||
|
||||
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
|
||||
{
|
||||
var path = fileSystemInfo.FullName;
|
||||
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
|
||||
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService);
|
||||
|
||||
if (hasMusic)
|
||||
{
|
||||
if (parser.IsMultiPart(path))
|
||||
{
|
||||
logger.LogDebug("Found multi-disc folder: {Path}", path);
|
||||
_logger.LogDebug("Found multi-disc folder: {Path}", path);
|
||||
Interlocked.Increment(ref discSubfolderCount);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -19,27 +20,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="config">The configuration manager.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MusicArtistResolver(
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config)
|
||||
NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
var directoryService = args.DirectoryService;
|
||||
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
|
||||
|
||||
// If we contain an album assume we are an artist folder
|
||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DiscUtils.Udf;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
protected BaseVideoResolver(ILibraryManager libraryManager)
|
||||
protected BaseVideoResolver(NamingOptions namingOptions)
|
||||
{
|
||||
LibraryManager = libraryManager;
|
||||
NamingOptions = namingOptions;
|
||||
}
|
||||
|
||||
protected ILibraryManager LibraryManager { get; }
|
||||
protected NamingOptions NamingOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
@@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
||||
where TVideoType : Video, new()
|
||||
{
|
||||
var namingOptions = LibraryManager.GetNamingOptions();
|
||||
var namingOptions = NamingOptions;
|
||||
|
||||
// If the path is a file check for a matching extensions
|
||||
if (args.IsDirectory)
|
||||
@@ -138,7 +139,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
|
||||
if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub)
|
||||
{
|
||||
var path = args.Path;
|
||||
|
||||
@@ -267,7 +268,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
protected void Set3DFormat(Video video)
|
||||
{
|
||||
var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
|
||||
var result = Format3DParser.Parse(video.Path, NamingOptions);
|
||||
|
||||
Set3DFormat(video, result.Is3D, result.Format3D);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
public class GenericVideoResolver<T> : BaseVideoResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
public GenericVideoResolver(ILibraryManager libraryManager)
|
||||
: base(libraryManager)
|
||||
public GenericVideoResolver(NamingOptions namingOptions)
|
||||
: base(namingOptions)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -25,6 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly StackResolver _stackResolver;
|
||||
|
||||
private string[] _validCollectionTypes = new[]
|
||||
{
|
||||
@@ -38,12 +40,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
|
||||
: base(libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
: base(namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_stackResolver = new StackResolver(NamingOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,9 +92,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
var files = args.GetActualFileSystemChildren().ToList();
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -258,9 +259,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = LibraryManager.GetNamingOptions();
|
||||
|
||||
var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
|
||||
var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList();
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
@@ -438,7 +437,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
if (result.Items.Count == 1)
|
||||
{
|
||||
var videoPath = result.Items[0].Path;
|
||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name));
|
||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
|
||||
|
||||
if (!hasPhotos)
|
||||
{
|
||||
@@ -511,9 +510,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
||||
|
||||
var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
|
||||
var result = _stackResolver.ResolveDirectories(folderPaths).ToList();
|
||||
|
||||
if (result.Count != 1)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
foreach (var siblingFile in files)
|
||||
{
|
||||
if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
|
||||
if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
|
||||
{
|
||||
ownedByMedia = true;
|
||||
break;
|
||||
|
||||
@@ -6,6 +6,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -16,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public class PhotoResolver : ItemResolver<Photo>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"folder",
|
||||
@@ -30,10 +33,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
"default"
|
||||
};
|
||||
|
||||
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
|
||||
if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -77,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename)
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
|
||||
{
|
||||
if (libraryManager.IsVideoFile(file))
|
||||
{
|
||||
return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
|
||||
}
|
||||
|
||||
return false;
|
||||
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
|
||||
internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
|
||||
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public EpisodeResolver(ILibraryManager libraryManager)
|
||||
: base(libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public EpisodeResolver(NamingOptions namingOptions)
|
||||
: base(namingOptions)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Globalization;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// </summary>
|
||||
public class SeasonResolver : GenericFolderResolver<Season>
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<SeasonResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="localization">The localization.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SeasonResolver(
|
||||
ILibraryManager libraryManager,
|
||||
NamingOptions namingOptions,
|
||||
ILocalizationManager localization,
|
||||
ILogger<SeasonResolver> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
_localization = localization;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
if (args.Parent is Series series && args.IsDirectory)
|
||||
{
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var path = args.Path;
|
||||
|
||||
@@ -65,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
var episodeInfo = resolver.Resolve(testPath, true);
|
||||
|
||||
if (episodeInfo != null)
|
||||
if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue)
|
||||
{
|
||||
if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
|
||||
path,
|
||||
episodeInfo.SeasonNumber.Value,
|
||||
episodeInfo.EpisodeNumber.Value);
|
||||
_logger.LogDebug(
|
||||
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
|
||||
path,
|
||||
episodeInfo.SeasonNumber.Value,
|
||||
episodeInfo.EpisodeNumber.Value);
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@@ -21,17 +26,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
public class SeriesResolver : GenericFolderResolver<Series>
|
||||
{
|
||||
private readonly ILogger<SeriesResolver> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,12 +59,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
return null;
|
||||
}
|
||||
|
||||
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_libraryManager.GetNamingOptions(), args.Path);
|
||||
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
|
||||
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
|
||||
var configuredContentType = args.GetConfiguredContentType();
|
||||
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Series
|
||||
@@ -91,7 +97,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
@@ -105,11 +111,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsSeriesFolder(
|
||||
private bool IsSeriesFolder(
|
||||
string path,
|
||||
IEnumerable<FileSystemMetadata> fileSystemChildren,
|
||||
ILogger<SeriesResolver> logger,
|
||||
ILibraryManager libraryManager,
|
||||
bool isTvContentType)
|
||||
{
|
||||
foreach (var child in fileSystemChildren)
|
||||
@@ -118,21 +122,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
if (IsSeasonFolder(child.FullName, isTvContentType))
|
||||
{
|
||||
logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string fullName = child.FullName;
|
||||
if (libraryManager.IsVideoFile(fullName))
|
||||
if (VideoResolver.IsVideoFile(path, _namingOptions))
|
||||
{
|
||||
if (isTvContentType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions);
|
||||
|
||||
@@ -145,7 +149,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug("{Path} is not a series folder.", path);
|
||||
_logger.LogDebug("{Path} is not a series folder.", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
{
|
||||
/// <summary>
|
||||
/// Class CollectionPostScanTask.
|
||||
/// </summary>
|
||||
public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly ILogger<CollectionPostScanTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="collectionManager">The collection manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CollectionPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ICollectionManager collectionManager,
|
||||
ILogger<CollectionPostScanTask> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_collectionManager = collectionManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
|
||||
|
||||
foreach (var library in _libraryManager.RootFolder.Children)
|
||||
{
|
||||
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var startIndex = 0;
|
||||
var pagesize = 1000;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var movies = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = new string[] { MediaType.Video },
|
||||
IncludeItemTypes = new[] { nameof(Movie) },
|
||||
IsVirtualItem = false,
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
Parent = library,
|
||||
StartIndex = startIndex,
|
||||
Limit = pagesize,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var m in movies)
|
||||
{
|
||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
||||
{
|
||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||
{
|
||||
movieList.Add(movie.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movies.Count < pagesize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
startIndex += pagesize;
|
||||
}
|
||||
}
|
||||
|
||||
var numComplete = 0;
|
||||
var count = collectionNameMoviesMap.Count;
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(BoxSet) },
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
|
||||
{
|
||||
try
|
||||
{
|
||||
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
|
||||
if (boxSet == null)
|
||||
{
|
||||
// won't automatically create collection if only one movie in it
|
||||
if (movieIds.Count >= 2)
|
||||
{
|
||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
Name = collectionName,
|
||||
IsLocked = true
|
||||
});
|
||||
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
|
||||
inputTempFile,
|
||||
targetFile.Replace("\"", "\\\""), // Escape quotes in filename
|
||||
targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
|
||||
videoArgs,
|
||||
GetAudioArgs(mediaSource),
|
||||
subtitleArgs,
|
||||
|
||||
@@ -9,8 +9,10 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -20,7 +22,6 @@ using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
@@ -35,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
private readonly ILogger<SchedulesDirect> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
@@ -43,12 +43,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
public SchedulesDirect(
|
||||
ILogger<SchedulesDirect> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ICryptoProvider cryptoProvider)
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -104,11 +102,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
};
|
||||
|
||||
var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
|
||||
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
|
||||
_logger.LogDebug("Request string for schedules is: {@RequestString}", requestList);
|
||||
|
||||
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
|
||||
options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
|
||||
options.Headers.TryAddWithoutValidation("token", token);
|
||||
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -124,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||
programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
|
||||
programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
|
||||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||
|
||||
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -169,12 +165,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
const double DesiredAspect = 2.0 / 3;
|
||||
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
|
||||
GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
|
||||
GetProgramImage(ApiUrl, allImages, DesiredAspect);
|
||||
|
||||
const double WideAspect = 16.0 / 9;
|
||||
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
|
||||
|
||||
// Don't supply the same image twice
|
||||
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
||||
@@ -182,7 +178,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
programEntry.ThumbImage = null;
|
||||
}
|
||||
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
|
||||
|
||||
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
||||
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
||||
@@ -403,7 +399,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return info;
|
||||
}
|
||||
|
||||
private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
|
||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
|
||||
{
|
||||
var match = images
|
||||
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
||||
@@ -648,7 +644,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
|
||||
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
|
||||
var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password));
|
||||
// TODO: remove ToLower when Convert.ToHexString supports lowercase
|
||||
// Schedules Direct requires the hex to be lowercase
|
||||
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
client.Connect(remoteIp, HdHomeRunPort);
|
||||
await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false);
|
||||
|
||||
using var stream = client.GetStream();
|
||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
// #EXTINF:0,84.0 - VOX Schweiz
|
||||
if (!string.IsNullOrWhiteSpace(nameInExtInf))
|
||||
{
|
||||
var numberIndex = nameInExtInf.IndexOf(' ');
|
||||
var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal);
|
||||
if (numberIndex > 0)
|
||||
{
|
||||
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
"Collections": "التجميعات",
|
||||
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
||||
"DeviceOnlineWithName": "{0} متصل",
|
||||
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
|
||||
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
|
||||
"Favorites": "مفضلات",
|
||||
"Folders": "المجلدات",
|
||||
"Genres": "التضنيفات",
|
||||
"HeaderAlbumArtists": "ألبوم الفنان",
|
||||
"HeaderAlbumArtists": "فناني الألبوم",
|
||||
"HeaderContinueWatching": "استمر بالمشاهدة",
|
||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||
|
||||
1
Emby.Server.Implementations/Localization/Core/as.json
Normal file
1
Emby.Server.Implementations/Localization/Core/as.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"Sync": "Сінхранізацыя",
|
||||
"Playlists": "Плэйліст"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"HeaderLiveTV": "TV en Directe",
|
||||
"HeaderNextUp": "A continuació",
|
||||
"HeaderRecordingGroups": "Grups d'Enregistrament",
|
||||
"HomeVideos": "Vídeos domèstics",
|
||||
"HomeVideos": "Vídeos Domèstics",
|
||||
"Inherit": "Hereta",
|
||||
"ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
|
||||
@@ -39,7 +39,7 @@
|
||||
"MixedContent": "Contingut barrejat",
|
||||
"Movies": "Pel·lícules",
|
||||
"Music": "Música",
|
||||
"MusicVideos": "Vídeos musicals",
|
||||
"MusicVideos": "Vídeos Musicals",
|
||||
"NameInstallFailed": "Instalació de {0} fallida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada Desconeguda",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Oblíbené",
|
||||
"Folders": "Složky",
|
||||
"Genres": "Žánry",
|
||||
"HeaderAlbumArtists": "Album umělce",
|
||||
"HeaderAlbumArtists": "Umělci alba",
|
||||
"HeaderContinueWatching": "Pokračovat ve sledování",
|
||||
"HeaderFavoriteAlbums": "Oblíbená alba",
|
||||
"HeaderFavoriteArtists": "Oblíbení interpreti",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Favourites",
|
||||
"Folders": "Folders",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artist's Album",
|
||||
"HeaderAlbumArtists": "Album artists",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderFavoriteAlbums": "Favourite Albums",
|
||||
"HeaderFavoriteArtists": "Favourite Artists",
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
"Default": "Default",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"FailedLoginAttemptWithUserName": "Failed login try from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"Forced": "Forced",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artist's Album",
|
||||
"HeaderAlbumArtists": "Album artists",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
"HeaderFavoriteArtists": "Favorite Artists",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"ItemAddedWithName": "{0} aldonis al la plurmediteko",
|
||||
"HeaderLiveTV": "TV-etero",
|
||||
"HeaderContinueWatching": "Daŭrigi Spektadon",
|
||||
"HeaderAlbumArtists": "Albumo de artisto",
|
||||
"HeaderAlbumArtists": "Artistoj de albumo",
|
||||
"Folders": "Dosierujoj",
|
||||
"DeviceOnlineWithName": "{0} estas konektita",
|
||||
"Default": "Defaŭlte",
|
||||
@@ -104,7 +104,7 @@
|
||||
"TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
|
||||
"TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
|
||||
"TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
|
||||
"TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn",
|
||||
"TaskRefreshChapterImages": "Eltiri Ĉapitrajn Bildojn",
|
||||
"TaskCleanCache": "Malplenigi Staplan Katalogon",
|
||||
"TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
|
||||
"PluginUpdatedWithName": "{0} estis ĝisdatigita",
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artista del álbum",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderAlbumArtists": "Artistas del álbum",
|
||||
"HeaderContinueWatching": "Seguir viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
"HeaderFavoriteShows": "Programas favoritos",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderAlbumArtists": "Artistas del álbum",
|
||||
"HeaderAlbumArtists": "Artistas de álbum",
|
||||
"Genres": "Géneros",
|
||||
"Folders": "Carpetas",
|
||||
"Favorites": "Favoritos",
|
||||
@@ -29,7 +29,7 @@
|
||||
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
|
||||
"TaskRefreshChannels": "Actualizar canales",
|
||||
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
|
||||
"TaskCleanTranscode": "Limpiar directorio de transcodificado",
|
||||
"TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
|
||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
|
||||
"TaskUpdatePlugins": "Actualizar complementos",
|
||||
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
|
||||
@@ -105,7 +105,7 @@
|
||||
"Inherit": "Heredar",
|
||||
"HomeVideos": "Videos caseros",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
|
||||
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
|
||||
"DeviceOnlineWithName": "{0} está conectado",
|
||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
@@ -114,10 +114,10 @@
|
||||
"Application": "Aplicación",
|
||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
|
||||
"TaskCleanActivityLog": "Limpiar Registro de Actividades",
|
||||
"TaskCleanActivityLog": "Limpiar registro de actividades",
|
||||
"Undefined": "Sin definir",
|
||||
"Forced": "Forzado",
|
||||
"Default": "Por Defecto",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
|
||||
"TaskOptimizeDatabase": "Optimización de base de datos"
|
||||
"Default": "Por defecto",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
|
||||
"TaskOptimizeDatabase": "Optimizar base de datos"
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"ValueSpecialEpisodeName": "Eriepisood - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
|
||||
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
|
||||
"UserPasswordChangedWithName": "Kasutaja {0} parooli on muudetud",
|
||||
"UserLockedOutWithName": "Kasutaja {0} on lukustatud",
|
||||
"UserDeletedWithName": "Kasutaja {0} on kustutatud",
|
||||
"UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
|
||||
"UserLockedOutWithName": "Kasutaja {0} lukustati",
|
||||
"UserDeletedWithName": "Kasutaja {0} kustutati",
|
||||
"UserCreatedWithName": "Kasutaja {0} on loodud",
|
||||
"ScheduledTaskStartedWithName": "{0} käivitati",
|
||||
"ProviderValue": "Allikas: {0}",
|
||||
@@ -54,9 +54,9 @@
|
||||
"Plugin": "Plugin",
|
||||
"Playlists": "Pleilistid",
|
||||
"Photos": "Fotod",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video taasesitus on peatatud",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
|
||||
"NotificationOptionVideoPlayback": "Video taasesitus algas",
|
||||
"NotificationOptionUserLockedOut": "Kasutaja on lukustatud",
|
||||
"NotificationOptionUserLockedOut": "Kasutaja lukustati",
|
||||
"NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus",
|
||||
"NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine",
|
||||
"NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus",
|
||||
@@ -66,7 +66,7 @@
|
||||
"NotificationOptionNewLibraryContent": "Lisati uut sisu",
|
||||
"NotificationOptionInstallationFailed": "Paigaldamine nurjus",
|
||||
"NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud",
|
||||
"NotificationOptionAudioPlaybackStopped": "Heli taasesitus peatati",
|
||||
"NotificationOptionAudioPlaybackStopped": "Heli taasesitus lõppes",
|
||||
"NotificationOptionAudioPlayback": "Heli taasesitus algas",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
|
||||
@@ -97,12 +97,12 @@
|
||||
"HeaderFavoriteArtists": "Lemmikesitajad",
|
||||
"HeaderFavoriteAlbums": "Lemmikalbumid",
|
||||
"HeaderContinueWatching": "Jätka vaatamist",
|
||||
"HeaderAlbumArtists": "Albumi esitaja",
|
||||
"HeaderAlbumArtists": "Albumi esitajad",
|
||||
"Genres": "Žanrid",
|
||||
"Forced": "Sunnitud",
|
||||
"Folders": "Kaustad",
|
||||
"Favorites": "Lemmikud",
|
||||
"FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}",
|
||||
"FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus",
|
||||
"DeviceOnlineWithName": "{0} on ühendatud",
|
||||
"DeviceOfflineWithName": "{0} katkestas ühenduse",
|
||||
"Default": "Vaikimisi",
|
||||
@@ -114,5 +114,10 @@
|
||||
"Artists": "Esitajad",
|
||||
"Application": "Rakendus",
|
||||
"AppDeviceValues": "Rakendus: {0}, seade: {1}",
|
||||
"Albums": "Albumid"
|
||||
"Albums": "Albumid",
|
||||
"UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
|
||||
"UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
|
||||
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
|
||||
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
|
||||
"Books": "کتابها",
|
||||
"CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
|
||||
"Channels": "کانالها",
|
||||
"Channels": "کانالها",
|
||||
"ChapterNameValue": "قسمت {0}",
|
||||
"Collections": "مجموعهها",
|
||||
"DeviceOfflineWithName": "ارتباط {0} قطع شد",
|
||||
@@ -37,7 +37,7 @@
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
|
||||
"MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
|
||||
"MixedContent": "محتوای مخلوط",
|
||||
"Movies": "فیلمها",
|
||||
"Movies": "فیلم ها",
|
||||
"Music": "موسیقی",
|
||||
"MusicVideos": "موزیک ویدیوها",
|
||||
"NameInstallFailed": "{0} نصب با مشکل مواجه شد",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Favoris",
|
||||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artistes de l'album",
|
||||
"HeaderAlbumArtists": "Artistes d'album",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes préférés",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Favoriti",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Žanrovi",
|
||||
"HeaderAlbumArtists": "Album od izvođača",
|
||||
"HeaderAlbumArtists": "Izvođači albuma",
|
||||
"HeaderContinueWatching": "Nastavi gledati",
|
||||
"HeaderFavoriteAlbums": "Omiljeni albumi",
|
||||
"HeaderFavoriteArtists": "Omiljeni izvođači",
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
"Collections": "Gyűjtemények",
|
||||
"DeviceOfflineWithName": "{0} kijelentkezett",
|
||||
"DeviceOnlineWithName": "{0} belépett",
|
||||
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}",
|
||||
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
|
||||
"Favorites": "Kedvencek",
|
||||
"Folders": "Könyvtárak",
|
||||
"Genres": "Műfajok",
|
||||
"HeaderAlbumArtists": "Előadó albumai",
|
||||
"HeaderAlbumArtists": "Album előadó(k)",
|
||||
"HeaderContinueWatching": "Megtekintés folytatása",
|
||||
"HeaderFavoriteAlbums": "Kedvenc albumok",
|
||||
"HeaderFavoriteArtists": "Kedvenc előadók",
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
|
||||
"Latest": "Terbaru",
|
||||
"LabelIpAddressValue": "Alamat IP: {0}",
|
||||
"ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
|
||||
"ItemRemovedWithName": "{0} sudah dihapus dari pustaka",
|
||||
"ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
|
||||
"Inherit": "Warisan",
|
||||
"HomeVideos": "Video Rumah",
|
||||
"Inherit": "Warisi",
|
||||
"HomeVideos": "Video Rumahan",
|
||||
"HeaderRecordingGroups": "Grup Rekaman",
|
||||
"HeaderNextUp": "Selanjutnya",
|
||||
"HeaderLiveTV": "TV Live",
|
||||
@@ -73,7 +73,7 @@
|
||||
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
|
||||
"NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
|
||||
"NewVersionIsAvailable": "Versi baru dari Jellyfin Server sudah tersedia untuk diunduh.",
|
||||
"NameSeasonUnknown": "Musim tak diketahui",
|
||||
"NameSeasonNumber": "Musim {0}",
|
||||
"NameInstallFailed": "{0} penginstalan gagal",
|
||||
@@ -117,5 +117,7 @@
|
||||
"TaskCleanActivityLog": "Bersihkan Log Aktivitas",
|
||||
"Undefined": "Tidak terdefinisi",
|
||||
"Forced": "Dipaksa",
|
||||
"Default": "Bawaan"
|
||||
"Default": "Bawaan",
|
||||
"TaskOptimizeDatabaseDescription": "Rapihkan basis data dan membersihkan ruang kosong. Menjalankan tugas ini setelah memindai pustaka atau melakukan perubahan lain yang menyiratkan modifikasi basis data dapat meningkatkan kinerja.",
|
||||
"TaskOptimizeDatabase": "Optimalkan basis data"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Preferiti",
|
||||
"Folders": "Cartelle",
|
||||
"Genres": "Generi",
|
||||
"HeaderAlbumArtists": "Artisti dell'Album",
|
||||
"HeaderAlbumArtists": "Artisti dell'album",
|
||||
"HeaderContinueWatching": "Continua a guardare",
|
||||
"HeaderFavoriteAlbums": "Album Preferiti",
|
||||
"HeaderFavoriteArtists": "Artisti Preferiti",
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
"Collections": "コレクション",
|
||||
"DeviceOfflineWithName": "{0} が切断されました",
|
||||
"DeviceOnlineWithName": "{0} が接続されました",
|
||||
"FailedLoginAttemptWithUserName": "ログインを試行しましたが {0}によって失敗しました",
|
||||
"FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました",
|
||||
"Favorites": "お気に入り",
|
||||
"Folders": "フォルダー",
|
||||
"Genres": "ジャンル",
|
||||
"HeaderAlbumArtists": "アーティストのアルバム",
|
||||
"HeaderAlbumArtists": "アルバムアーティスト",
|
||||
"HeaderContinueWatching": "視聴を続ける",
|
||||
"HeaderFavoriteAlbums": "お気に入りのアルバム",
|
||||
"HeaderFavoriteArtists": "お気に入りのアーティスト",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Tañdaulylar",
|
||||
"Folders": "Qaltalar",
|
||||
"Genres": "Janrlar",
|
||||
"HeaderAlbumArtists": "Oryndauşynyñ älbomy",
|
||||
"HeaderAlbumArtists": "Älbom oryndauşylary",
|
||||
"HeaderContinueWatching": "Qaraudy jalğastyru",
|
||||
"HeaderFavoriteAlbums": "Tañdauly älbomdar",
|
||||
"HeaderFavoriteArtists": "Tañdauly oryndauşylar",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Albums": "Albumai",
|
||||
"AppDeviceValues": "Programa: {0}, Įrenginys: {1}",
|
||||
"Application": "Programa",
|
||||
"Application": "Programėlė",
|
||||
"Artists": "Atlikėjai",
|
||||
"AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
|
||||
"Books": "Knygos",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"PluginUninstalledWithName": "{0} беше успешно деинсталирано",
|
||||
"PluginInstalledWithName": "{0} беше успешно инсталирано",
|
||||
"Plugin": "Додатоци",
|
||||
"Playlists": "Листи",
|
||||
"Playlists": "Плејлисти",
|
||||
"Photos": "Слики",
|
||||
"NotificationOptionVideoPlaybackStopped": "Видео стопирано",
|
||||
"NotificationOptionVideoPlayback": "Видео пуштено",
|
||||
@@ -50,7 +50,7 @@
|
||||
"HeaderFavoriteEpisodes": "Омилени Епизоди",
|
||||
"HeaderFavoriteArtists": "Омилени Изведувачи",
|
||||
"HeaderFavoriteAlbums": "Омилени Албуми",
|
||||
"HeaderContinueWatching": "Продолжи со гледање",
|
||||
"HeaderContinueWatching": "Продолжи со Гледање",
|
||||
"HeaderAlbumArtists": "Изведувачи од Албуми",
|
||||
"Genres": "Жанрови",
|
||||
"Folders": "Папки",
|
||||
@@ -97,5 +97,8 @@
|
||||
"TasksChannelsCategory": "Интернет Канали",
|
||||
"TasksApplicationCategory": "Апликација",
|
||||
"TasksLibraryCategory": "Библиотека",
|
||||
"TasksMaintenanceCategory": "Одржување"
|
||||
"TasksMaintenanceCategory": "Одржување",
|
||||
"Undefined": "Недефинирано",
|
||||
"Forced": "Принудно",
|
||||
"Default": "Зададено"
|
||||
}
|
||||
|
||||
14
Emby.Server.Implementations/Localization/Core/mn.json
Normal file
14
Emby.Server.Implementations/Localization/Core/mn.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"HeaderNextUp": "Дараах",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрөл зүйл",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Artists": "Зураачуд",
|
||||
"Albums": "Цомгууд"
|
||||
}
|
||||
@@ -37,9 +37,9 @@
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
|
||||
"MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
|
||||
"MixedContent": "Kandungan campuran",
|
||||
"Movies": "Filem",
|
||||
"Movies": "Filem-filem",
|
||||
"Music": "Muzik",
|
||||
"MusicVideos": "Muzik video",
|
||||
"MusicVideos": "",
|
||||
"NameInstallFailed": "{0} pemasangan gagal",
|
||||
"NameSeasonNumber": "Musim {0}",
|
||||
"NameSeasonUnknown": "Musim Tidak Diketahui",
|
||||
@@ -53,43 +53,43 @@
|
||||
"NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah",
|
||||
"NotificationOptionPluginError": "Kegagalan plugin",
|
||||
"NotificationOptionPluginInstalled": "Plugin telah dipasang",
|
||||
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
|
||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
|
||||
"NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
|
||||
"NotificationOptionServerRestartRequired": "",
|
||||
"NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
|
||||
"NotificationOptionUserLockedOut": "Pengguna telah dikunci",
|
||||
"NotificationOptionVideoPlayback": "Ulangmain video bermula",
|
||||
"NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
|
||||
"Photos": "Gambar-gambar",
|
||||
"Playlists": "Senarai main",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"PluginInstalledWithName": "{0} telah dipasang",
|
||||
"PluginUninstalledWithName": "{0} telah dinyahpasang",
|
||||
"PluginUpdatedWithName": "{0} telah dikemaskini",
|
||||
"ProviderValue": "Pembekal: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} gagal",
|
||||
"ScheduledTaskStartedWithName": "{0} bermula",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"Shows": "Series",
|
||||
"ServerNameNeedsToBeRestarted": "{0} perlu di ulangmula",
|
||||
"Shows": "Tayangan",
|
||||
"Songs": "Lagu-lagu",
|
||||
"StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
|
||||
"Sync": "Sync",
|
||||
"Sync": "",
|
||||
"System": "Sistem",
|
||||
"TvShows": "TV Shows",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted",
|
||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
||||
"TvShows": "Tayangan TV",
|
||||
"User": "Pengguna",
|
||||
"UserCreatedWithName": "Pengguna {0} telah diwujudkan",
|
||||
"UserDeletedWithName": "Pengguna {0} telah dipadamkan",
|
||||
"UserDownloadingItemWithValues": "{0} sedang memuat turun {1}",
|
||||
"UserLockedOutWithName": "Pengguna {0} telah dikunci",
|
||||
"UserOfflineFromDevice": "{0} telah terputus dari {1}",
|
||||
"UserOnlineFromDevice": "{0} berada dalam talian dari {1}",
|
||||
"UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}",
|
||||
"UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"UserStartedPlayingItemWithValues": "{0} sedang dimainkan {1} pada {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} telah tamat dimainkan {1} pada {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} telah ditambah ke media library anda",
|
||||
"ValueSpecialEpisodeName": "Khas - {0}",
|
||||
"VersionNumber": "Versi {0}",
|
||||
"TaskCleanActivityLog": "Log Aktiviti Bersih",
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ",
|
||||
"UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ",
|
||||
"User": "प्रयोगकर्ता",
|
||||
"PluginInstalledWithName": "",
|
||||
"PluginInstalledWithName": "{0} सभएको थियो",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
|
||||
"Songs": "गीतहरू",
|
||||
"Shows": "शोहरू",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Favorieten",
|
||||
"Folders": "Mappen",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artiests Album",
|
||||
"HeaderAlbumArtists": "Album Artiesten",
|
||||
"HeaderContinueWatching": "Kijken hervatten",
|
||||
"HeaderFavoriteAlbums": "Favoriete albums",
|
||||
"HeaderFavoriteArtists": "Favoriete artiesten",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"TasksLibraryCategory": "ਲਾਇਬ੍ਰੇਰੀ",
|
||||
"TasksMaintenanceCategory": "ਰੱਖ-ਰਖਾਅ",
|
||||
"VersionNumber": "ਵਰਜਨ {0}",
|
||||
"ValueSpecialEpisodeName": "ਵਿਸ਼ੇਸ਼ - {0}",
|
||||
"ValueSpecialEpisodeName": "ਖਾਸ - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
|
||||
"UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
|
||||
@@ -43,8 +43,8 @@
|
||||
"Sync": "ਸਿੰਕ",
|
||||
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
|
||||
"StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.",
|
||||
"Songs": "ਗਾਣੇ",
|
||||
"Shows": "ਸ਼ੋਅਜ਼",
|
||||
"Songs": "ਗਾਣੇਂ",
|
||||
"Shows": "ਸ਼ੋਅ",
|
||||
"ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
|
||||
"ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
|
||||
"ScheduledTaskFailedWithName": "{0} ਅਸਫਲ",
|
||||
@@ -53,7 +53,7 @@
|
||||
"PluginUninstalledWithName": "{0} ਅਣਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ ਸੀ",
|
||||
"PluginInstalledWithName": "{0} ਲਗਾਇਆ ਗਿਆ ਸੀ",
|
||||
"Plugin": "ਪਲੱਗਇਨ",
|
||||
"Playlists": "ਪਲੇਲਿਸਟਸ",
|
||||
"Playlists": "ਪਲੇਸੂਚੀਆਂ",
|
||||
"Photos": "ਫੋਟੋਆਂ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
|
||||
"NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
|
||||
@@ -102,13 +102,13 @@
|
||||
"HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
|
||||
"Genres": "ਸ਼ੈਲੀਆਂ",
|
||||
"Forced": "ਮਜਬੂਰ",
|
||||
"Folders": "ਫੋਲਡਰ",
|
||||
"Folders": "ਫੋਲਡਰਸ",
|
||||
"Favorites": "ਮਨਪਸੰਦ",
|
||||
"FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}",
|
||||
"DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
|
||||
"DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
|
||||
"Default": "ਮੂਲ",
|
||||
"Collections": "ਸੰਗ੍ਰਹਿ",
|
||||
"Default": "ਡਿਫੌਲਟ",
|
||||
"Collections": "ਸੰਗ੍ਰਹਿਣ",
|
||||
"ChapterNameValue": "ਅਧਿਆਇ {0}",
|
||||
"Channels": "ਚੈਨਲ",
|
||||
"CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Ulubione",
|
||||
"Folders": "Foldery",
|
||||
"Genres": "Gatunki",
|
||||
"HeaderAlbumArtists": "Album artysty",
|
||||
"HeaderAlbumArtists": "Wykonawcy albumów",
|
||||
"HeaderContinueWatching": "Kontynuuj odtwarzanie",
|
||||
"HeaderFavoriteAlbums": "Ulubione albumy",
|
||||
"HeaderFavoriteArtists": "Ulubieni wykonawcy",
|
||||
@@ -47,7 +47,7 @@
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację",
|
||||
"NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki",
|
||||
"NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane",
|
||||
"NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane",
|
||||
"NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego",
|
||||
"NotificationOptionInstallationFailed": "Nieudana instalacja",
|
||||
"NotificationOptionNewLibraryContent": "Dodano nową zawartość",
|
||||
@@ -98,7 +98,7 @@
|
||||
"TaskRefreshChannels": "Odśwież kanały",
|
||||
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
||||
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.",
|
||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
|
||||
"TaskUpdatePlugins": "Aktualizuj pluginy",
|
||||
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
||||
"TaskRefreshPeople": "Odśwież obsadę",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"Books": "Libros",
|
||||
"AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
|
||||
"Artists": "Artistas"
|
||||
"Artists": "Artistas",
|
||||
"Songs": "Shantees",
|
||||
"Albums": "Ships"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Pastas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas do Álbum",
|
||||
"HeaderAlbumArtists": "Álbum do Artista",
|
||||
"HeaderContinueWatching": "Continuar a Ver",
|
||||
"HeaderFavoriteAlbums": "Álbuns Favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"HeaderFavoriteArtists": "Artiști Favoriți",
|
||||
"HeaderFavoriteAlbums": "Albume Favorite",
|
||||
"HeaderContinueWatching": "Vizionează în continuare",
|
||||
"HeaderAlbumArtists": "Album Artiști",
|
||||
"HeaderAlbumArtists": "Albume Artiști",
|
||||
"Genres": "Genuri",
|
||||
"Folders": "Dosare",
|
||||
"Favorites": "Favorite",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"NameSeasonUnknown": "Sezon i panjohur",
|
||||
"NameSeasonNumber": "Sezoni {0}",
|
||||
"NameInstallFailed": "Instalimi i {0} dështoi",
|
||||
"MusicVideos": "Video muzikore",
|
||||
"MusicVideos": "Video Muzikore",
|
||||
"Music": "Muzikë",
|
||||
"Movies": "Filmat",
|
||||
"MixedContent": "Përmbajtje e përzier",
|
||||
@@ -96,7 +96,7 @@
|
||||
"HeaderFavoriteArtists": "Artistët e preferuar",
|
||||
"HeaderFavoriteAlbums": "Albumet e preferuar",
|
||||
"HeaderContinueWatching": "Vazhdo të shikosh",
|
||||
"HeaderAlbumArtists": "Artistët e Albumeve",
|
||||
"HeaderAlbumArtists": "Artistët e albumeve",
|
||||
"Genres": "Zhanret",
|
||||
"Folders": "Skedarët",
|
||||
"Favorites": "Të preferuarat",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"NameSeasonUnknown": "Непозната сезона",
|
||||
"NameSeasonNumber": "Сезона {0}",
|
||||
"NameInstallFailed": "Инсталација {0} није успела",
|
||||
"MusicVideos": "Музички спотови",
|
||||
"MusicVideos": "Музички видео",
|
||||
"Music": "Музика",
|
||||
"Movies": "Филмови",
|
||||
"MixedContent": "Мешовит садржај",
|
||||
@@ -64,7 +64,7 @@
|
||||
"ItemRemovedWithName": "{0} уклоњено из библиотеке",
|
||||
"ItemAddedWithName": "{0} додато у библиотеку",
|
||||
"Inherit": "Наследи",
|
||||
"HomeVideos": "Кућни видео",
|
||||
"HomeVideos": "Кућни Видео",
|
||||
"HeaderRecordingGroups": "Групе снимања",
|
||||
"HeaderNextUp": "Следи",
|
||||
"HeaderLiveTV": "ТВ уживо",
|
||||
@@ -117,5 +117,6 @@
|
||||
"TaskCleanActivityLog": "Очисти историју активности",
|
||||
"Undefined": "Недефинисано",
|
||||
"Forced": "Принудно",
|
||||
"Default": "Подразумевано"
|
||||
"Default": "Подразумевано",
|
||||
"TaskOptimizeDatabase": "Оптимизуј датабазу"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Favoriter",
|
||||
"Folders": "Mappar",
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Artistens album",
|
||||
"HeaderAlbumArtists": "Albumsartister",
|
||||
"HeaderContinueWatching": "Fortsätt kolla",
|
||||
"HeaderFavoriteAlbums": "Favoritalbum",
|
||||
"HeaderFavoriteArtists": "Favoritartister",
|
||||
@@ -96,8 +96,8 @@
|
||||
"TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter",
|
||||
"TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.",
|
||||
"TaskRefreshChannels": "Uppdatera kanaler",
|
||||
"TaskCleanTranscodeDescription": "Raderar transkodningsfiler som är mer än en dag gamla.",
|
||||
"TaskCleanTranscode": "Töm transkodningskatalog",
|
||||
"TaskCleanTranscodeDescription": "Raderar omkodningsfiler som är mer än en dag gamla.",
|
||||
"TaskCleanTranscode": "Töm omkodningskatalog",
|
||||
"TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.",
|
||||
"TaskUpdatePlugins": "Uppdatera insticksprogram",
|
||||
"TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.",
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
"HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
|
||||
"HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்",
|
||||
"HeaderContinueWatching": "தொடர்ந்து பார்",
|
||||
"HeaderAlbumArtists": "இசைக் கலைஞர்கள்",
|
||||
"HeaderAlbumArtists": "கலைஞரின் ஆல்பம்",
|
||||
"Genres": "வகைகள்",
|
||||
"Favorites": "பிடித்தவை",
|
||||
"ChapterNameValue": "அத்தியாயம் {0}",
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
{}
|
||||
{
|
||||
"ValueSpecialEpisodeName": "ప్రత్యేక - {0}",
|
||||
"Sync": "సమకాలీకరించు",
|
||||
"Songs": "పాటలు",
|
||||
"Shows": "ప్రదర్శనలు",
|
||||
"Playlists": "ప్లేజాబితాలు",
|
||||
"Photos": "ఫోటోలు",
|
||||
"MusicVideos": "మ్యూజిక్ వీడియోలు",
|
||||
"Music": "సంగీతం",
|
||||
"Movies": "సినిమాలు",
|
||||
"HeaderContinueWatching": "చూడటం కొనసాగించండి",
|
||||
"HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు",
|
||||
"Genres": "శైలులు",
|
||||
"Forced": "బలవంతంగా",
|
||||
"Folders": "ఫోల్డర్లు",
|
||||
"Favorites": "ఇష్టమైనవి",
|
||||
"Default": "డిఫాల్ట్",
|
||||
"Collections": "సేకరణలు",
|
||||
"Channels": "ఛానెల్లు",
|
||||
"Books": "పుస్తకాలు",
|
||||
"Artists": "కళాకారులు",
|
||||
"Albums": "ఆల్బమ్లు"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"HeaderFavoriteArtists": "Улюблені виконавці",
|
||||
"HeaderFavoriteAlbums": "Улюблені альбоми",
|
||||
"HeaderContinueWatching": "Продовжити перегляд",
|
||||
"HeaderAlbumArtists": "Виконавці альбомів",
|
||||
"HeaderAlbumArtists": "Виконавці альбому",
|
||||
"Genres": "Жанри",
|
||||
"Folders": "Каталоги",
|
||||
"Favorites": "Улюблені",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"Favorites": "Yêu Thích",
|
||||
"Folders": "Thư Mục",
|
||||
"Genres": "Thể Loại",
|
||||
"HeaderAlbumArtists": "Album Nghệ sĩ",
|
||||
"HeaderAlbumArtists": "Album nghệ sĩ",
|
||||
"HeaderContinueWatching": "Xem Tiếp",
|
||||
"HeaderLiveTV": "TV Trực Tiếp",
|
||||
"Movies": "Phim",
|
||||
@@ -95,7 +95,7 @@
|
||||
"ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
|
||||
"ItemAddedWithName": "{0} được thêm vào thư viện",
|
||||
"Inherit": "Thừa hưởng",
|
||||
"HomeVideos": "Video nhà",
|
||||
"HomeVideos": "Video Nhà",
|
||||
"HeaderRecordingGroups": "Nhóm Ghi Video",
|
||||
"HeaderNextUp": "Tiếp Theo",
|
||||
"HeaderFavoriteSongs": "Bài Hát Yêu Thích",
|
||||
@@ -103,7 +103,7 @@
|
||||
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
||||
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
||||
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
||||
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
|
||||
"FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
|
||||
"DeviceOnlineWithName": "{0} đã kết nối",
|
||||
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
||||
"ChapterNameValue": "Phân Cảnh {0}",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "合集",
|
||||
"DeviceOfflineWithName": "{0} 已断开",
|
||||
"DeviceOnlineWithName": "{0} 已连接",
|
||||
"FailedLoginAttemptWithUserName": "来自 {0} 的失败登入",
|
||||
"FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
|
||||
"Favorites": "我的最爱",
|
||||
"Folders": "文件夹",
|
||||
"Genres": "风格",
|
||||
|
||||
@@ -373,60 +373,75 @@ namespace Emby.Server.Implementations.Localization
|
||||
public IEnumerable<LocalizationOption> GetLocalizationOptions()
|
||||
{
|
||||
yield return new LocalizationOption("Afrikaans", "af");
|
||||
yield return new LocalizationOption("Arabic", "ar");
|
||||
yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG");
|
||||
yield return new LocalizationOption("Catalan", "ca");
|
||||
yield return new LocalizationOption("Chinese (Hong Kong)", "zh-HK");
|
||||
yield return new LocalizationOption("Chinese Simplified", "zh-CN");
|
||||
yield return new LocalizationOption("Chinese Traditional", "zh-TW");
|
||||
yield return new LocalizationOption("Croatian", "hr");
|
||||
yield return new LocalizationOption("Czech", "cs");
|
||||
yield return new LocalizationOption("Danish", "da");
|
||||
yield return new LocalizationOption("Dutch", "nl");
|
||||
yield return new LocalizationOption("العربية", "ar");
|
||||
yield return new LocalizationOption("Беларуская", "be");
|
||||
yield return new LocalizationOption("Български", "bg-BG");
|
||||
yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn");
|
||||
yield return new LocalizationOption("Català", "ca");
|
||||
yield return new LocalizationOption("Čeština", "cs");
|
||||
yield return new LocalizationOption("Cymraeg", "cy");
|
||||
yield return new LocalizationOption("Dansk", "da");
|
||||
yield return new LocalizationOption("Deutsch", "de");
|
||||
yield return new LocalizationOption("English (United Kingdom)", "en-GB");
|
||||
yield return new LocalizationOption("English (United States)", "en-US");
|
||||
yield return new LocalizationOption("English", "en-US");
|
||||
yield return new LocalizationOption("Ελληνικά", "el");
|
||||
yield return new LocalizationOption("Esperanto", "eo");
|
||||
yield return new LocalizationOption("Estonian", "et");
|
||||
yield return new LocalizationOption("Finnish", "fi");
|
||||
yield return new LocalizationOption("French", "fr");
|
||||
yield return new LocalizationOption("French (Canada)", "fr-CA");
|
||||
yield return new LocalizationOption("German", "de");
|
||||
yield return new LocalizationOption("Greek", "el");
|
||||
yield return new LocalizationOption("Hebrew", "he");
|
||||
yield return new LocalizationOption("Hungarian", "hu");
|
||||
yield return new LocalizationOption("Icelandic", "is");
|
||||
yield return new LocalizationOption("Indonesian", "id");
|
||||
yield return new LocalizationOption("Italian", "it");
|
||||
yield return new LocalizationOption("Japanese", "ja");
|
||||
yield return new LocalizationOption("Kazakh", "kk");
|
||||
yield return new LocalizationOption("Korean", "ko");
|
||||
yield return new LocalizationOption("Latvian", "lv");
|
||||
yield return new LocalizationOption("Lithuanian", "lt-LT");
|
||||
yield return new LocalizationOption("Malay", "ms");
|
||||
yield return new LocalizationOption("Malayalam", "ml");
|
||||
yield return new LocalizationOption("Norwegian Bokmål", "nb");
|
||||
yield return new LocalizationOption("Norwegian Nynorsk", "nn");
|
||||
yield return new LocalizationOption("Persian", "fa");
|
||||
yield return new LocalizationOption("Polish", "pl");
|
||||
yield return new LocalizationOption("Portuguese", "pt");
|
||||
yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR");
|
||||
yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT");
|
||||
yield return new LocalizationOption("Romanian", "ro");
|
||||
yield return new LocalizationOption("Russian", "ru");
|
||||
yield return new LocalizationOption("Serbian", "sr");
|
||||
yield return new LocalizationOption("Slovak", "sk");
|
||||
yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI");
|
||||
yield return new LocalizationOption("Spanish", "es");
|
||||
yield return new LocalizationOption("Spanish (Argentina)", "es-AR");
|
||||
yield return new LocalizationOption("Spanish (Latin America)", "es-419");
|
||||
yield return new LocalizationOption("Spanish (Mexico)", "es-MX");
|
||||
yield return new LocalizationOption("Swedish", "sv");
|
||||
yield return new LocalizationOption("Swiss German", "gsw");
|
||||
yield return new LocalizationOption("Tamil", "ta");
|
||||
yield return new LocalizationOption("Telugu", "te");
|
||||
yield return new LocalizationOption("Turkish", "tr");
|
||||
yield return new LocalizationOption("Español", "es");
|
||||
yield return new LocalizationOption("Español americano", "es_419");
|
||||
yield return new LocalizationOption("Español (Argentina)", "es-AR");
|
||||
yield return new LocalizationOption("Español (Dominicana)", "es_DO");
|
||||
yield return new LocalizationOption("Español (México)", "es-MX");
|
||||
yield return new LocalizationOption("Eesti", "et");
|
||||
yield return new LocalizationOption("فارسی", "fa");
|
||||
yield return new LocalizationOption("Suomi", "fi");
|
||||
yield return new LocalizationOption("Filipino", "fil");
|
||||
yield return new LocalizationOption("Français", "fr");
|
||||
yield return new LocalizationOption("Français (Canada)", "fr-CA");
|
||||
yield return new LocalizationOption("Galego", "gl");
|
||||
yield return new LocalizationOption("Schwiizerdütsch", "gsw");
|
||||
yield return new LocalizationOption("עִבְרִית", "he");
|
||||
yield return new LocalizationOption("हिन्दी", "hi");
|
||||
yield return new LocalizationOption("Hrvatski", "hr");
|
||||
yield return new LocalizationOption("Magyar", "hu");
|
||||
yield return new LocalizationOption("Bahasa Indonesia", "id");
|
||||
yield return new LocalizationOption("Íslenska", "is");
|
||||
yield return new LocalizationOption("Italiano", "it");
|
||||
yield return new LocalizationOption("日本語", "ja");
|
||||
yield return new LocalizationOption("Qazaqşa", "kk");
|
||||
yield return new LocalizationOption("한국어", "ko");
|
||||
yield return new LocalizationOption("Lietuvių", "lt");
|
||||
yield return new LocalizationOption("Latviešu", "lv");
|
||||
yield return new LocalizationOption("Македонски", "mk");
|
||||
yield return new LocalizationOption("മലയാളം", "ml");
|
||||
yield return new LocalizationOption("मराठी", "mr");
|
||||
yield return new LocalizationOption("Bahasa Melayu", "ms");
|
||||
yield return new LocalizationOption("Norsk bokmål", "nb");
|
||||
yield return new LocalizationOption("नेपाली", "ne");
|
||||
yield return new LocalizationOption("Nederlands", "nl");
|
||||
yield return new LocalizationOption("Norsk nynorsk", "nn");
|
||||
yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa");
|
||||
yield return new LocalizationOption("Polski", "pl");
|
||||
yield return new LocalizationOption("Pirate", "pr");
|
||||
yield return new LocalizationOption("Português", "pt");
|
||||
yield return new LocalizationOption("Português (Brasil)", "pt-BR");
|
||||
yield return new LocalizationOption("Português (Portugal)", "pt-PT");
|
||||
yield return new LocalizationOption("Românește", "ro");
|
||||
yield return new LocalizationOption("Русский", "ru");
|
||||
yield return new LocalizationOption("Slovenčina", "sk");
|
||||
yield return new LocalizationOption("Slovenščina", "sl-SI");
|
||||
yield return new LocalizationOption("Shqip", "sq");
|
||||
yield return new LocalizationOption("Српски", "sr");
|
||||
yield return new LocalizationOption("Svenska", "sv");
|
||||
yield return new LocalizationOption("தமிழ்", "ta");
|
||||
yield return new LocalizationOption("తెలుగు", "te");
|
||||
yield return new LocalizationOption("ภาษาไทย", "th");
|
||||
yield return new LocalizationOption("Türkçe", "tr");
|
||||
yield return new LocalizationOption("Українська", "uk");
|
||||
yield return new LocalizationOption("اُردُو", "ur_PK");
|
||||
yield return new LocalizationOption("Tiếng Việt", "vi");
|
||||
yield return new LocalizationOption("Ukrainian", "uk");
|
||||
yield return new LocalizationOption("汉语 (简化字)", "zh-CN");
|
||||
yield return new LocalizationOption("漢語 (繁体字)", "zh-TW");
|
||||
yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri);
|
||||
string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
|
||||
|
||||
if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase))
|
||||
if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int Compare(BaseItem? x, BaseItem? y)
|
||||
{
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int Compare(BaseItem? x, BaseItem? y)
|
||||
{
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,9 +35,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
/// <returns>System.String.</returns>
|
||||
private static string? GetValue(BaseItem? x)
|
||||
{
|
||||
var audio = x as Audio;
|
||||
|
||||
return audio == null ? string.Empty : audio.Album;
|
||||
return x is Audio audio ? audio.Album : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
/// <inheritdoc />
|
||||
public int Compare(BaseItem? x, BaseItem? y)
|
||||
{
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase);
|
||||
return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int Compare(BaseItem x, BaseItem y)
|
||||
{
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
|
||||
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetValue(BaseItem item)
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Sorting
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
|
||||
return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.Udp
|
||||
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
}
|
||||
|
||||
private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
|
||||
private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
|
||||
{
|
||||
string? localUrl = _config[AddressOverrideConfigKey];
|
||||
if (string.IsNullOrEmpty(localUrl))
|
||||
@@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.Udp
|
||||
|
||||
try
|
||||
{
|
||||
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
|
||||
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.Udp
|
||||
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
|
||||
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await RespondToV2Message(text, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
|
||||
await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (SocketException ex)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
#pragma warning disable CA1813 // Avoid unsealed attributes
|
||||
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Produces file attribute of "image/*".
|
||||
/// </summary>
|
||||
public class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||
public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||
{
|
||||
private const string ContentType = "image/*";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes
|
||||
/// <summary>
|
||||
/// Identifies an action that supports the HTTP GET method.
|
||||
/// </summary>
|
||||
public class HttpSubscribeAttribute : HttpMethodAttribute
|
||||
public sealed class HttpSubscribeAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes
|
||||
/// <summary>
|
||||
/// Identifies an action that supports the HTTP GET method.
|
||||
/// </summary>
|
||||
public class HttpUnsubscribeAttribute : HttpMethodAttribute
|
||||
public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Jellyfin.Api.Attributes
|
||||
/// Attribute to mark a parameter as obsolete.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class ParameterObsoleteAttribute : Attribute
|
||||
public sealed class ParameterObsoleteAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Produces file attribute of "image/*".
|
||||
/// </summary>
|
||||
public class ProducesAudioFileAttribute : ProducesFileAttribute
|
||||
public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
|
||||
{
|
||||
private const string ContentType = "audio/*";
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
#pragma warning disable CA1813 // Avoid unsealed attributes
|
||||
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Produces file attribute of "image/*".
|
||||
/// </summary>
|
||||
public class ProducesImageFileAttribute : ProducesFileAttribute
|
||||
public sealed class ProducesImageFileAttribute : ProducesFileAttribute
|
||||
{
|
||||
private const string ContentType = "image/*";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Produces file attribute of "image/*".
|
||||
/// </summary>
|
||||
public class ProducesPlaylistFileAttribute : ProducesFileAttribute
|
||||
public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
|
||||
{
|
||||
private const string ContentType = "application/x-mpegURL";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Produces file attribute of "video/*".
|
||||
/// </summary>
|
||||
public class ProducesVideoFileAttribute : ProducesFileAttribute
|
||||
public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
|
||||
{
|
||||
private const string ContentType = "video/*";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Mime;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
@@ -7,7 +6,6 @@ using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.ClientLogDtos;
|
||||
using MediaBrowser.Controller.ClientEvent;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.ClientLog;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -37,54 +35,6 @@ namespace Jellyfin.Api.Controllers
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post event from client.
|
||||
/// </summary>
|
||||
/// <param name="clientLogEventDto">The client log dto.</param>
|
||||
/// <response code="204">Event logged.</response>
|
||||
/// <response code="403">Event logging disabled.</response>
|
||||
/// <returns>Submission status.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult LogEvent([FromBody] ClientLogEventDto clientLogEventDto)
|
||||
{
|
||||
if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var (clientName, clientVersion, userId, deviceId) = GetRequestInformation();
|
||||
Log(clientLogEventDto, userId, clientName, clientVersion, deviceId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk post events from client.
|
||||
/// </summary>
|
||||
/// <param name="clientLogEventDtos">The list of client log dtos.</param>
|
||||
/// <response code="204">All events logged.</response>
|
||||
/// <response code="403">Event logging disabled.</response>
|
||||
/// <returns>Submission status.</returns>
|
||||
[HttpPost("Bulk")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult LogEvents([FromBody] ClientLogEventDto[] clientLogEventDtos)
|
||||
{
|
||||
if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var (clientName, clientVersion, userId, deviceId) = GetRequestInformation();
|
||||
foreach (var dto in clientLogEventDtos)
|
||||
{
|
||||
Log(dto, userId, clientName, clientVersion, deviceId);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload a document.
|
||||
/// </summary>
|
||||
@@ -111,39 +61,20 @@ namespace Jellyfin.Api.Controllers
|
||||
return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
|
||||
}
|
||||
|
||||
var (clientName, clientVersion, _, _) = GetRequestInformation();
|
||||
var (clientName, clientVersion) = GetRequestInformation();
|
||||
var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(new ClientLogDocumentResponseDto(fileName));
|
||||
}
|
||||
|
||||
private void Log(
|
||||
ClientLogEventDto dto,
|
||||
Guid userId,
|
||||
string clientName,
|
||||
string clientVersion,
|
||||
string deviceId)
|
||||
{
|
||||
_clientEventLogger.Log(new ClientLogEvent(
|
||||
dto.Timestamp,
|
||||
dto.Level,
|
||||
userId,
|
||||
clientName,
|
||||
clientVersion,
|
||||
deviceId,
|
||||
dto.Message));
|
||||
}
|
||||
|
||||
private (string ClientName, string ClientVersion, Guid UserId, string DeviceId) GetRequestInformation()
|
||||
private (string ClientName, string ClientVersion) GetRequestInformation()
|
||||
{
|
||||
var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client";
|
||||
var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User)
|
||||
? "apikey"
|
||||
: ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version";
|
||||
var userId = ClaimHelpers.GetUserId(HttpContext.User) ?? Guid.Empty;
|
||||
var deviceId = ClaimHelpers.GetDeviceId(HttpContext.User) ?? "unknown-device-id";
|
||||
|
||||
return (clientName, clientVersion, userId, deviceId);
|
||||
return (clientName, clientVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@@ -30,7 +28,6 @@ namespace Jellyfin.Api.Controllers
|
||||
public class ItemLookupController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<ItemLookupController> _logger;
|
||||
@@ -39,19 +36,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
|
||||
public ItemLookupController(
|
||||
IProviderManager providerManager,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<ItemLookupController> logger)
|
||||
{
|
||||
_providerManager = providerManager;
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
|
||||
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
@@ -33,6 +34,7 @@ namespace Jellyfin.Api.Controllers
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly ILogger<ItemsController> _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemsController"/> class.
|
||||
@@ -42,18 +44,21 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
public ItemsController(
|
||||
IUserManager userManager,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localization,
|
||||
IDtoService dtoService,
|
||||
ILogger<ItemsController> logger)
|
||||
ILogger<ItemsController> logger,
|
||||
ISessionManager sessionManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_dtoService = dtoService;
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -763,6 +768,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
|
||||
/// <response code="200">Items returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
|
||||
[HttpGet("Users/{userId}/Items/Resume")]
|
||||
@@ -781,7 +787,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool excludeActiveSessions = false)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var parentIdGuid = parentId ?? Guid.Empty;
|
||||
@@ -801,6 +808,15 @@ namespace Jellyfin.Api.Controllers
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var excludeItemIds = Array.Empty<Guid>();
|
||||
if (excludeActiveSessions)
|
||||
{
|
||||
excludeItemIds = _sessionManager.Sessions
|
||||
.Where(s => s.UserId == userId && s.NowPlayingItem != null)
|
||||
.Select(s => s.NowPlayingItem.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
|
||||
@@ -817,7 +833,8 @@ namespace Jellyfin.Api.Controllers
|
||||
AncestorIds = ancestorIds,
|
||||
IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
|
||||
ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
|
||||
SearchTerm = searchTerm
|
||||
SearchTerm = searchTerm,
|
||||
ExcludeItemIds = excludeItemIds
|
||||
});
|
||||
|
||||
var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace Jellyfin.Api.Controllers
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PersonsController"/> class.
|
||||
@@ -34,17 +33,14 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||
public PersonsController(
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService,
|
||||
IUserManager userManager,
|
||||
IUserDataManager userDataManager)
|
||||
IUserManager userManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly IPluginManager _pluginManager;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,16 +35,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
||||
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
public PluginsController(
|
||||
IInstallationManager installationManager,
|
||||
IPluginManager pluginManager,
|
||||
IConfigurationManager config)
|
||||
IPluginManager pluginManager)
|
||||
{
|
||||
_installationManager = installationManager;
|
||||
_pluginManager = pluginManager;
|
||||
_serializerOptions = JsonDefaults.Options;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,17 +37,14 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public RemoteImageController(
|
||||
IProviderManager providerManager,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
_providerManager = providerManager;
|
||||
_applicationPaths = applicationPaths;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
|
||||
NetworkConfiguration settings = _config.GetNetworkConfiguration();
|
||||
settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
|
||||
settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
|
||||
_config.SaveConfiguration("network", settings);
|
||||
_config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -451,7 +451,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
|
||||
|
||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
|
||||
if (liveStreamInfo == null)
|
||||
@@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers
|
||||
// Static remote stream
|
||||
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
|
||||
@@ -484,7 +484,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
|
||||
var isTranscodeCached = outputPathExists && transcodingJob != null;
|
||||
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
|
||||
|
||||
// Static stream
|
||||
if (@static.HasValue && @static.Value)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// A static class for copying matching properties from one object to another.
|
||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||
/// </summary>
|
||||
public static class ClassMigrationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for 'Object' that copies the properties to a destination object.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="destination">The destination.</param>
|
||||
public static void CopyProperties(this object source, object destination)
|
||||
{
|
||||
// If any this null throw an exception.
|
||||
if (source == null || destination == null)
|
||||
{
|
||||
throw new ArgumentException("Source or/and Destination Objects are null");
|
||||
}
|
||||
|
||||
// Getting the Types of the objects.
|
||||
Type typeDest = destination.GetType();
|
||||
Type typeSrc = source.GetType();
|
||||
|
||||
// Iterate the Properties of the source instance and populate them from their destination counterparts.
|
||||
PropertyInfo[] srcProps = typeSrc.GetProperties();
|
||||
foreach (PropertyInfo srcProp in srcProps)
|
||||
{
|
||||
if (!srcProp.CanRead)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetProperty = typeDest.GetProperty(srcProp.Name);
|
||||
if (targetProperty == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!targetProperty.CanWrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var obj = targetProperty.GetSetMethod(true);
|
||||
if (obj != null && obj.IsPrivate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var target = targetProperty.GetSetMethod();
|
||||
if (target != null && (target.Attributes & MethodAttributes.Static) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Passed all tests, lets set the value.
|
||||
targetProperty.SetValue(destination, srcProp.GetValue(source, null), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ namespace Jellyfin.Api.Helpers
|
||||
private readonly TranscodingJobDto? _job;
|
||||
private readonly TranscodingJobHelper? _transcodingJobHelper;
|
||||
private readonly int _timeoutMs;
|
||||
private int _bytesWritten;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
@@ -71,53 +70,58 @@ namespace Jellyfin.Api.Helpers
|
||||
/// <inheritdoc />
|
||||
public override void Flush()
|
||||
{
|
||||
_stream.Flush();
|
||||
// Not supported
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
=> Read(buffer.AsSpan(offset, count));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
return _stream.Read(buffer, offset, count);
|
||||
int totalBytesRead = 0;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
while (KeepReading(stopwatch.ElapsedMilliseconds))
|
||||
{
|
||||
totalBytesRead += _stream.Read(buffer);
|
||||
if (totalBytesRead > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
UpdateBytesWritten(totalBytesRead);
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int totalBytesRead = 0;
|
||||
int remainingBytesToRead = count;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
int newOffset = offset;
|
||||
while (remainingBytesToRead > 0)
|
||||
while (KeepReading(stopwatch.ElapsedMilliseconds))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
remainingBytesToRead -= bytesRead;
|
||||
newOffset += bytesRead;
|
||||
|
||||
if (bytesRead > 0)
|
||||
totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (totalBytesRead > 0)
|
||||
{
|
||||
_bytesWritten += bytesRead;
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (_job != null)
|
||||
{
|
||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely
|
||||
if (_job?.HasExited ?? stopwatch.ElapsedMilliseconds > _timeoutMs)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
UpdateBytesWritten(totalBytesRead);
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
@@ -159,5 +163,19 @@ namespace Jellyfin.Api.Helpers
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBytesWritten(int totalBytesRead)
|
||||
{
|
||||
if (_job != null)
|
||||
{
|
||||
_job.BytesDownloaded += totalBytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
private bool KeepReading(long elapsed)
|
||||
{
|
||||
// If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely
|
||||
return !_job?.HasExited ?? elapsed < _timeoutMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user