mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-21 01:24:44 +01:00
Merge branch 'master' into NetworkPR2
This commit is contained in:
@@ -139,23 +139,21 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Class to register.</typeparam>
|
||||
public virtual void RegisterConfiguration<T>()
|
||||
where T : IConfigurationFactory
|
||||
{
|
||||
if (!typeof(IConfigurationFactory).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
throw new ArgumentException("Parameter does not implement IConfigurationFactory");
|
||||
}
|
||||
|
||||
IConfigurationFactory factory = (IConfigurationFactory)Activator.CreateInstance(typeof(T));
|
||||
IConfigurationFactory factory = Activator.CreateInstance<T>();
|
||||
|
||||
if (_configurationFactories == null)
|
||||
{
|
||||
_configurationFactories = new IConfigurationFactory[] { factory };
|
||||
_configurationFactories = new[] { factory };
|
||||
}
|
||||
else
|
||||
{
|
||||
var list = _configurationFactories.ToList<IConfigurationFactory>();
|
||||
list.Add(factory);
|
||||
_configurationFactories = list.ToArray();
|
||||
var oldLen = _configurationFactories.Length;
|
||||
var arr = new IConfigurationFactory[oldLen + 1];
|
||||
_configurationFactories.CopyTo(arr, 0);
|
||||
arr[oldLen] = factory;
|
||||
_configurationFactories = arr;
|
||||
}
|
||||
|
||||
_configurationStores = _configurationFactories
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.AppBase
|
||||
@@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
configuration = Activator.CreateInstance(type);
|
||||
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||
@@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
|
||||
// If the file didn't exist before, or if something has changed, re-save
|
||||
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
// Save it after load in case we got new items
|
||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -31,7 +30,6 @@ using Emby.Server.Implementations.Cryptography;
|
||||
using Emby.Server.Implementations.Data;
|
||||
using Emby.Server.Implementations.Devices;
|
||||
using Emby.Server.Implementations.Dto;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.Security;
|
||||
using Emby.Server.Implementations.IO;
|
||||
using Emby.Server.Implementations.Library;
|
||||
@@ -133,7 +131,6 @@ namespace Emby.Server.Implementations
|
||||
private IMediaEncoder _mediaEncoder;
|
||||
private ISessionManager _sessionManager;
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private string[] _urlPrefixes;
|
||||
|
||||
/// <summary>
|
||||
@@ -510,24 +507,11 @@ namespace Emby.Server.Implementations
|
||||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||
}
|
||||
|
||||
if (Plugins != null)
|
||||
{
|
||||
var pluginBuilder = new StringBuilder();
|
||||
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
pluginBuilder.Append(plugin.Name)
|
||||
.Append(' ')
|
||||
.Append(plugin.Version)
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
|
||||
}
|
||||
|
||||
DiscoverTypes();
|
||||
|
||||
RegisterServices();
|
||||
|
||||
RegisterPluginServices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -792,10 +776,24 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
_plugins = GetExports<IPlugin>()
|
||||
.Select(LoadPlugin)
|
||||
.Where(i => i != null)
|
||||
.ToArray();
|
||||
|
||||
if (Plugins != null)
|
||||
{
|
||||
var pluginBuilder = new StringBuilder();
|
||||
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
pluginBuilder.Append(plugin.Name)
|
||||
.Append(' ')
|
||||
.Append(plugin.Version)
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
|
||||
}
|
||||
|
||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||
|
||||
Resolve<ILibraryManager>().AddParts(
|
||||
@@ -825,21 +823,6 @@ namespace Emby.Server.Implementations
|
||||
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
|
||||
}
|
||||
|
||||
private IPlugin LoadPlugin(IPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.RegisterServices(ServiceCollection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
|
||||
return null;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers the types.
|
||||
/// </summary>
|
||||
@@ -850,6 +833,22 @@ namespace Emby.Server.Implementations
|
||||
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
||||
}
|
||||
|
||||
private void RegisterPluginServices()
|
||||
{
|
||||
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
|
||||
{
|
||||
try
|
||||
{
|
||||
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
|
||||
instance.RegisterServices(ServiceCollection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
foreach (var ass in assemblies)
|
||||
@@ -1005,79 +1004,59 @@ namespace Emby.Server.Implementations
|
||||
|
||||
protected abstract void RestartInternal();
|
||||
|
||||
/// <summary>
|
||||
/// Comparison function used in <see cref="GetPlugins" />.
|
||||
/// </summary>
|
||||
/// <param name="a">Item to compare.</param>
|
||||
/// <param name="b">Item to compare with.</param>
|
||||
/// <returns>Boolean result of the operation.</returns>
|
||||
private static int VersionCompare(
|
||||
(Version PluginVersion, string Name, string Path) a,
|
||||
(Version PluginVersion, string Name, string Path) b)
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
|
||||
{
|
||||
int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
|
||||
|
||||
if (compare == 0)
|
||||
var minimumVersion = new Version(0, 0, 0, 1);
|
||||
var versions = new List<LocalPlugin>();
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return a.PluginVersion.CompareTo(b.PluginVersion);
|
||||
// Plugin path doesn't exist, don't try to enumerate subfolders.
|
||||
return Enumerable.Empty<LocalPlugin>();
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of plugins to install.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to check.</param>
|
||||
/// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
|
||||
/// <returns>Enumerable list of dlls to load.</returns>
|
||||
private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
|
||||
{
|
||||
var dllList = new List<string>();
|
||||
var versions = new List<(Version PluginVersion, string Name, string Path)>();
|
||||
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
|
||||
string metafile;
|
||||
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
try
|
||||
{
|
||||
metafile = Path.Combine(dir, "meta.json");
|
||||
var metafile = Path.Combine(dir, "meta.json");
|
||||
if (File.Exists(metafile))
|
||||
{
|
||||
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
|
||||
|
||||
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
|
||||
{
|
||||
targetAbi = new Version(0, 0, 0, 1);
|
||||
targetAbi = minimumVersion;
|
||||
}
|
||||
|
||||
if (!Version.TryParse(manifest.Version, out var version))
|
||||
{
|
||||
version = new Version(0, 0, 0, 1);
|
||||
version = minimumVersion;
|
||||
}
|
||||
|
||||
if (ApplicationVersion >= targetAbi)
|
||||
{
|
||||
// Only load Plugins if the plugin is built for this version or below.
|
||||
versions.Add((version, manifest.Name, dir));
|
||||
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No metafile, so lets see if the folder is versioned.
|
||||
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
|
||||
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
|
||||
|
||||
int versionIndex = dir.LastIndexOf('_');
|
||||
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
|
||||
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
|
||||
{
|
||||
// Versioned folder.
|
||||
versions.Add((ver, metafile, dir));
|
||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
|
||||
versions.Add((new Version(0, 0, 0, 1), metafile, dir));
|
||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1088,14 +1067,14 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
string lastName = string.Empty;
|
||||
versions.Sort(VersionCompare);
|
||||
versions.Sort(LocalPlugin.Compare);
|
||||
// Traverse backwards through the list.
|
||||
// The first item will be the latest version.
|
||||
for (int x = versions.Count - 1; x >= 0; x--)
|
||||
{
|
||||
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
|
||||
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
|
||||
lastName = versions[x].Name;
|
||||
continue;
|
||||
}
|
||||
@@ -1103,6 +1082,7 @@ namespace Emby.Server.Implementations
|
||||
if (!string.IsNullOrEmpty(lastName) && cleanup)
|
||||
{
|
||||
// Attempt a cleanup of old folders.
|
||||
versions.RemoveAt(x);
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Deleting {Path}", versions[x].Path);
|
||||
@@ -1115,7 +1095,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
return dllList;
|
||||
return versions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1126,21 +1106,24 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
|
||||
foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
Assembly plugAss;
|
||||
try
|
||||
foreach (var file in plugin.DllFiles)
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
Assembly plugAss;
|
||||
try
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
|
||||
var all = channels;
|
||||
var totalCount = all.Count;
|
||||
|
||||
if (query.StartIndex.HasValue)
|
||||
if (query.StartIndex.HasValue || query.Limit.HasValue)
|
||||
{
|
||||
all = all.Skip(query.StartIndex.Value).ToList();
|
||||
int startIndex = query.StartIndex ?? 0;
|
||||
int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
|
||||
all = all.GetRange(startIndex, count);
|
||||
}
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
all = all.Take(query.Limit.Value).ToList();
|
||||
}
|
||||
|
||||
var returnItems = all.ToArray();
|
||||
|
||||
if (query.RefreshLatestChannelItems)
|
||||
{
|
||||
foreach (var item in returnItems)
|
||||
foreach (var item in all)
|
||||
{
|
||||
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
@@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
return new QueryResult<Channel>
|
||||
{
|
||||
Items = returnItems,
|
||||
Items = all,
|
||||
TotalRecordCount = totalCount
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using static MediaBrowser.Common.Cryptography.Constants;
|
||||
|
||||
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
}
|
||||
|
||||
using var h = HashAlgorithm.Create(hashMethod);
|
||||
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
|
||||
if (salt.Length == 0)
|
||||
{
|
||||
return h.ComputeHash(bytes);
|
||||
|
||||
@@ -1007,7 +1007,7 @@ namespace Emby.Server.Implementations.Data
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
@@ -1057,7 +1057,7 @@ namespace Emby.Server.Implementations.Data
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
|
||||
var list = new List<ItemImageInfo>();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
@@ -1096,7 +1096,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
public ItemImageInfo ItemImageInfoFromValueString(string value)
|
||||
{
|
||||
var parts = value.Split(new[] { '*' }, StringSplitOptions.None);
|
||||
var parts = value.Split('*', StringSplitOptions.None);
|
||||
|
||||
if (parts.Length < 3)
|
||||
{
|
||||
@@ -1532,7 +1532,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
if (!reader.IsDBNull(index))
|
||||
{
|
||||
item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
IEnumerable<MetadataField> GetLockedFields(string s)
|
||||
{
|
||||
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (Enum.TryParse(i, true, out MetadataField parsedValue))
|
||||
{
|
||||
@@ -1612,7 +1612,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
if (!reader.IsDBNull(index))
|
||||
{
|
||||
item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
if (!reader.IsDBNull(index))
|
||||
{
|
||||
item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -1636,7 +1636,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
IEnumerable<TrailerType> GetTrailerTypes(string s)
|
||||
{
|
||||
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (Enum.TryParse(i, true, out TrailerType parsedValue))
|
||||
{
|
||||
@@ -1811,7 +1811,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
if (!reader.IsDBNull(index))
|
||||
{
|
||||
item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
|
||||
item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -1848,14 +1848,14 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
|
||||
{
|
||||
hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
if (string.IsNullOrEmpty(item.OfficialRating))
|
||||
{
|
||||
builder.Append("((OfficialRating is null) * 10)");
|
||||
builder.Append("(OfficialRating is null * 10)");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
|
||||
builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
|
||||
}
|
||||
|
||||
if (item.ProductionYear.HasValue)
|
||||
@@ -2416,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
|
||||
builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
|
||||
}
|
||||
|
||||
//// genres, tags
|
||||
builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)");
|
||||
// genres, tags, studios, person, year?
|
||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
|
||||
|
||||
if (item is MusicArtist)
|
||||
{
|
||||
// Match albums where the artist is AlbumArtist against other albums.
|
||||
// It is assumed that similar albums => similar artists.
|
||||
builder.Append(
|
||||
@"+ (WITH artistValues AS (
|
||||
SELECT DISTINCT albumValues.CleanValue
|
||||
FROM ItemValues albumValues
|
||||
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
|
||||
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
|
||||
), similarArtist AS (
|
||||
SELECT albumValues.ItemId
|
||||
FROM ItemValues albumValues
|
||||
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
|
||||
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
|
||||
) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
|
||||
}
|
||||
|
||||
builder.Append(") as SimilarityScore");
|
||||
|
||||
@@ -5002,26 +5020,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
|
||||
CheckDisposed();
|
||||
|
||||
var commandText = "select Distinct Name from People";
|
||||
var commandText = new StringBuilder("select Distinct p.Name from People p");
|
||||
|
||||
if (query.User != null && query.IsFavorite.HasValue)
|
||||
{
|
||||
commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
|
||||
commandText.Append(typeof(Person).FullName);
|
||||
commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
|
||||
}
|
||||
|
||||
var whereClauses = GetPeopleWhereClauses(query, null);
|
||||
|
||||
if (whereClauses.Count != 0)
|
||||
{
|
||||
commandText += " where " + string.Join(" AND ", whereClauses);
|
||||
commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
|
||||
}
|
||||
|
||||
commandText += " order by ListOrder";
|
||||
commandText.Append(" order by ListOrder");
|
||||
|
||||
if (query.Limit > 0)
|
||||
{
|
||||
commandText += " LIMIT " + query.Limit;
|
||||
commandText.Append(" LIMIT ").Append(query.Limit);
|
||||
}
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
var list = new List<string>();
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||
{
|
||||
// Run this again to bind the params
|
||||
GetPeopleWhereClauses(query, statement);
|
||||
@@ -5045,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
|
||||
CheckDisposed();
|
||||
|
||||
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People";
|
||||
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
|
||||
|
||||
var whereClauses = GetPeopleWhereClauses(query, null);
|
||||
|
||||
@@ -5087,19 +5112,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
if (!query.ItemId.Equals(Guid.Empty))
|
||||
{
|
||||
whereClauses.Add("ItemId=@ItemId");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
|
||||
}
|
||||
statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
|
||||
}
|
||||
|
||||
if (!query.AppearsInItemId.Equals(Guid.Empty))
|
||||
{
|
||||
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
|
||||
}
|
||||
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
|
||||
statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
|
||||
}
|
||||
|
||||
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
|
||||
@@ -5107,10 +5126,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
if (queryPersonTypes.Count == 1)
|
||||
{
|
||||
whereClauses.Add("PersonType=@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
else if (queryPersonTypes.Count > 1)
|
||||
{
|
||||
@@ -5124,10 +5140,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
if (queryExcludePersonTypes.Count == 1)
|
||||
{
|
||||
whereClauses.Add("PersonType<>@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
else if (queryExcludePersonTypes.Count > 1)
|
||||
{
|
||||
@@ -5139,19 +5152,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
if (query.MaxListOrder.HasValue)
|
||||
{
|
||||
whereClauses.Add("ListOrder<=@MaxListOrder");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
|
||||
}
|
||||
statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.NameContains))
|
||||
{
|
||||
whereClauses.Add("Name like @NameContains");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
whereClauses.Add("p.Name like @NameContains");
|
||||
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
|
||||
if (query.IsFavorite.HasValue)
|
||||
{
|
||||
whereClauses.Add("isFavorite=@IsFavorite");
|
||||
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
||||
}
|
||||
|
||||
if (query.User != null)
|
||||
{
|
||||
statement?.TryBind("@UserId", query.User.InternalId);
|
||||
}
|
||||
|
||||
return whereClauses;
|
||||
@@ -5420,6 +5438,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
|
||||
Tags = query.Tags,
|
||||
OfficialRatings = query.OfficialRatings,
|
||||
StudioIds = query.StudioIds,
|
||||
GenreIds = query.GenreIds,
|
||||
Genres = query.Genres,
|
||||
Years = query.Years,
|
||||
@@ -5592,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
return counts;
|
||||
}
|
||||
|
||||
var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToLookup(x => x);
|
||||
|
||||
foreach (var type in allTypes)
|
||||
|
||||
@@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
continue;
|
||||
}
|
||||
|
||||
var containers = container.Split(new[] { ',' });
|
||||
var containers = container.Split(',');
|
||||
if (containers.Length < 2)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.0" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.26.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
|
||||
@@ -48,10 +48,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||
{
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
if (auth?.User == null)
|
||||
if (!auth.IsAuthenticated)
|
||||
{
|
||||
return null;
|
||||
throw new AuthenticationException("Invalid token.");
|
||||
}
|
||||
|
||||
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
||||
if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.");
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(requestContext);
|
||||
var (authInfo, _) =
|
||||
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
@@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var (authInfo, originalAuthInfo) =
|
||||
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
if (originalAuthInfo != null)
|
||||
{
|
||||
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||
}
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||
in Dictionary<string, string> auth,
|
||||
in IHeaderDictionary headers,
|
||||
in IQueryCollection queryString)
|
||||
@@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
Device = device,
|
||||
DeviceId = deviceId,
|
||||
Version = version,
|
||||
Token = token
|
||||
Token = token,
|
||||
IsAuthenticated = false
|
||||
};
|
||||
|
||||
AuthenticationInfo originalAuthenticationInfo = null;
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
// Request doesn't contain a token.
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
|
||||
if (result.Items.Count > 0)
|
||||
{
|
||||
authInfo.IsAuthenticated = true;
|
||||
}
|
||||
|
||||
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
{
|
||||
var updateToken = false;
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
|
||||
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
var updateToken = false;
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
}
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
authInfo.IsApiKey = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
authInfo.IsApiKey = false;
|
||||
}
|
||||
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return (authInfo, originalAuthenticationInfo);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -238,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = authorizationHeader.Split(new[] { ' ' }, 2);
|
||||
var parts = authorizationHeader.Split(' ', 2);
|
||||
|
||||
// There should be at least to parts
|
||||
if (parts.Length != 2)
|
||||
@@ -262,11 +269,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
foreach (var item in parts)
|
||||
{
|
||||
var param = item.Trim().Split(new[] { '=' }, 2);
|
||||
var param = item.Trim().Split('=', 2);
|
||||
|
||||
if (param.Length == 2)
|
||||
{
|
||||
var value = NormalizeValue(param[1].Trim(new[] { '"' }));
|
||||
var value = NormalizeValue(param[1].Trim('"'));
|
||||
result[param[0]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
@@ -2440,6 +2441,21 @@ namespace Emby.Server.Implementations.Library
|
||||
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
|
||||
}
|
||||
|
||||
public BaseItem GetParentItem(string parentId, Guid? userId)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parentId))
|
||||
{
|
||||
return GetItemById(new Guid(parentId));
|
||||
}
|
||||
|
||||
if (userId.HasValue && userId != Guid.Empty)
|
||||
{
|
||||
return GetUserRootFolder();
|
||||
}
|
||||
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
@@ -2690,7 +2706,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var videos = videoListResolver.Resolve(fileSystemChildren);
|
||||
|
||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
|
||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (currentVideo != null)
|
||||
{
|
||||
@@ -2892,7 +2908,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
return item.GetImageInfo(image.Type, imageIndex);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (ex.StatusCode.HasValue
|
||||
&& (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
|
||||
|
||||
@@ -849,7 +849,7 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentException("Key can't be empty.", nameof(key));
|
||||
}
|
||||
|
||||
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
||||
var keys = key.Split(LiveStreamIdDelimeter, 2);
|
||||
|
||||
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
continue;
|
||||
}
|
||||
|
||||
var firstMedia = resolvedItem.Files.First();
|
||||
var firstMedia = resolvedItem.Files[0];
|
||||
|
||||
var libraryItem = new T
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
@@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
public SchedulesDirect(
|
||||
ILogger<SchedulesDirect> logger,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IApplicationHost appHost)
|
||||
IApplicationHost appHost,
|
||||
ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
}
|
||||
|
||||
private string UserAgent => _appHost.ApplicationUserAgent;
|
||||
@@ -587,7 +591,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
return result;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (ex.StatusCode.HasValue)
|
||||
{
|
||||
@@ -617,7 +621,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_tokens.Clear();
|
||||
|
||||
@@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
|
||||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
|
||||
string hashedPassword = Hex.Encode(hashedPasswordBytes);
|
||||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
@@ -705,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// Apparently we're supposed to swallow this
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
|
||||
|
||||
@@ -1429,7 +1429,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, ItemFields[] fields, User user = null)
|
||||
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
|
||||
{
|
||||
var programTuples = new List<Tuple<BaseItemDto, string, string>>();
|
||||
var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
|
||||
@@ -2208,7 +2208,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
/// <returns>Task.</returns>
|
||||
public Task ResetTuner(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
var parts = id.Split(new[] { '_' }, 2);
|
||||
var parts = id.Split('_', 2);
|
||||
|
||||
var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
return discoverResponse;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
|
||||
{
|
||||
@@ -663,7 +663,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
|
||||
info.DeviceId = modelInfo.DeviceID;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
|
||||
@@ -153,6 +153,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
if (string.IsNullOrEmpty(currentFile))
|
||||
{
|
||||
return (files.Last(), true);
|
||||
return (files[^1], true);
|
||||
}
|
||||
|
||||
var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
|
||||
|
||||
@@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
||||
|
||||
return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
return await new M3uParser(Logger, _httpClientFactory, _appHost)
|
||||
.Parse(info, channelIdPrefix, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
@@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
@@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
|
||||
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
|
||||
{
|
||||
// Read the file and display it line by line.
|
||||
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
|
||||
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
return GetChannels(reader, channelIdPrefix, tunerHostId);
|
||||
return GetChannels(reader, channelIdPrefix, info.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
|
||||
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetStreamAsync(url);
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
|
||||
if (!string.IsNullOrEmpty(info.UserAgent))
|
||||
{
|
||||
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
|
||||
}
|
||||
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(requestMessage, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Task.FromResult((Stream)File.OpenRead(url));
|
||||
return File.OpenRead(info.Url);
|
||||
}
|
||||
|
||||
private const string ExtInfPrefix = "#EXTINF:";
|
||||
@@ -153,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
||||
{
|
||||
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
|
||||
|
||||
string numberString = null;
|
||||
@@ -263,8 +273,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
|
||||
{
|
||||
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null;
|
||||
var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null;
|
||||
|
||||
// Check for channel number with the format from SatIp
|
||||
// #EXTINF:0,84. VOX Schweiz
|
||||
|
||||
@@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
var typeName = GetType().Name;
|
||||
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
// Response stream is disposed manually.
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
}
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TasksChannelsCategory": "Internetové kanály",
|
||||
"TasksApplicationCategory": "Aplikace",
|
||||
"TasksLibraryCategory": "Knihovna",
|
||||
"TasksMaintenanceCategory": "Údržba"
|
||||
"TasksMaintenanceCategory": "Údržba",
|
||||
"TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
|
||||
"TaskCleanActivityLog": "Smazat záznam aktivity"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TasksChannelsCategory": "Internet Kanäle",
|
||||
"TasksApplicationCategory": "Anwendung",
|
||||
"TasksLibraryCategory": "Bibliothek",
|
||||
"TasksMaintenanceCategory": "Wartung"
|
||||
"TasksMaintenanceCategory": "Wartung",
|
||||
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
||||
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TasksChannelsCategory": "Internet Channels",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TasksLibraryCategory": "Library",
|
||||
"TasksMaintenanceCategory": "Maintenance"
|
||||
"TasksMaintenanceCategory": "Maintenance",
|
||||
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
|
||||
"TaskCleanActivityLog": "Clean Activity Log"
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
"Channels": "Channels",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"Collections": "Collections",
|
||||
"Default": "Default",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"Forced": "Forced",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
@@ -77,6 +79,7 @@
|
||||
"Sync": "Sync",
|
||||
"System": "System",
|
||||
"TvShows": "TV Shows",
|
||||
"Undefined": "Undefined",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted",
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
|
||||
"Sync": "Sincronizar",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Programas de televisión",
|
||||
"TvShows": "Series",
|
||||
"User": "Usuario",
|
||||
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||
"UserDeletedWithName": "El usuario {0} ha sido borrado",
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskRefreshChannels": "Actualizar canales",
|
||||
"TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
|
||||
"TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos."
|
||||
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos.",
|
||||
"TaskCleanActivityLogDescription": "Elimina todos los registros de actividad anteriores a la fecha configurada.",
|
||||
"TaskCleanActivityLog": "Limpiar registro de actividad"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"VersionNumber": "Bersyon {0}",
|
||||
"ValueSpecialEpisodeName": "Espesyal - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library",
|
||||
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
|
||||
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
|
||||
"UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
|
||||
"UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
|
||||
@@ -61,8 +61,8 @@
|
||||
"Latest": "Pinakabago",
|
||||
"LabelRunningTimeValue": "Oras: {0}",
|
||||
"LabelIpAddressValue": "Ang IP Address ay {0}",
|
||||
"ItemRemovedWithName": "Naitanggal ang {0} sa library",
|
||||
"ItemAddedWithName": "Naidagdag ang {0} sa library",
|
||||
"ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
|
||||
"ItemAddedWithName": "Naidagdag ang {0} sa librerya",
|
||||
"Inherit": "Manahin",
|
||||
"HeaderRecordingGroups": "Pagtatalang Grupo",
|
||||
"HeaderNextUp": "Susunod",
|
||||
@@ -90,12 +90,29 @@
|
||||
"Application": "Aplikasyon",
|
||||
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
|
||||
"Albums": "Albums",
|
||||
"TaskRefreshLibrary": "Suriin ang nasa librerya",
|
||||
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata",
|
||||
"TaskRefreshLibrary": "Suriin and Librerya ng Medya",
|
||||
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
|
||||
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
|
||||
"TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
|
||||
"TasksChannelsCategory": "Palabas sa internet",
|
||||
"TasksLibraryCategory": "Librerya",
|
||||
"TasksMaintenanceCategory": "Pagpapanatili",
|
||||
"HomeVideos": "Sariling pelikula"
|
||||
"HomeVideos": "Sariling pelikula",
|
||||
"TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
|
||||
"TaskRefreshPeople": "I-refresh ang Tauhan",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
|
||||
"TaskDownloadMissingSubtitles": "I-download and nawawalang subtitles",
|
||||
"TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
|
||||
"TaskRefreshChannels": "I-refresh ang Channels",
|
||||
"TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
|
||||
"TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
|
||||
"TaskUpdatePlugins": "I-update ang Plugins",
|
||||
"TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
|
||||
"TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
|
||||
"TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
|
||||
"TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
|
||||
"TaskCleanCache": "Linisin and Direktoryo ng Cache",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
|
||||
"TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskCleanCache": "Vider le répertoire cache",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TasksLibraryCategory": "Bibliothèque",
|
||||
"TasksMaintenanceCategory": "Maintenance"
|
||||
"TasksMaintenanceCategory": "Maintenance",
|
||||
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
|
||||
"TaskCleanActivityLog": "Nettoyer le journal d'activité"
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"Artists": "Izvođači",
|
||||
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
|
||||
"Books": "Knjige",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavlje {0}",
|
||||
"Collections": "Kolekcije",
|
||||
"DeviceOfflineWithName": "{0} se odspojilo",
|
||||
"DeviceOnlineWithName": "{0} je spojeno",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
|
||||
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
|
||||
"Favorites": "Favoriti",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Žanrovi",
|
||||
@@ -23,95 +23,97 @@
|
||||
"HeaderFavoriteShows": "Omiljene serije",
|
||||
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||
"HeaderLiveTV": "TV uživo",
|
||||
"HeaderNextUp": "Sljedeće je",
|
||||
"HeaderNextUp": "Slijedi",
|
||||
"HeaderRecordingGroups": "Grupa snimka",
|
||||
"HomeVideos": "Kućni videi",
|
||||
"HomeVideos": "Kućni video",
|
||||
"Inherit": "Naslijedi",
|
||||
"ItemAddedWithName": "{0} je dodano u biblioteku",
|
||||
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
|
||||
"ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
|
||||
"LabelIpAddressValue": "IP adresa: {0}",
|
||||
"LabelRunningTimeValue": "Vrijeme rada: {0}",
|
||||
"Latest": "Najnovije",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
|
||||
"MessageApplicationUpdated": "Jellyfin server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
|
||||
"MixedContent": "Miješani sadržaj",
|
||||
"Movies": "Filmovi",
|
||||
"Music": "Glazba",
|
||||
"MusicVideos": "Glazbeni spotovi",
|
||||
"NameInstallFailed": "{0} neuspješnih instalacija",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Nepoznata sezona",
|
||||
"NameSeasonUnknown": "Sezona nepoznata",
|
||||
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
|
||||
"NotificationOptionInstallationFailed": "Instalacija neuspješna",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
|
||||
"NotificationOptionPluginError": "Dodatak otkazao",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
|
||||
"NotificationOptionInstallationFailed": "Instalacija nije uspjela",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
|
||||
"NotificationOptionPluginError": "Dodatak zakazao",
|
||||
"NotificationOptionPluginInstalled": "Dodatak instaliran",
|
||||
"NotificationOptionPluginUninstalled": "Dodatak uklonjen",
|
||||
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak",
|
||||
"NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera",
|
||||
"NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen",
|
||||
"NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
|
||||
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
|
||||
"NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
|
||||
"NotificationOptionTaskFailed": "Greška zakazanog zadatka",
|
||||
"NotificationOptionUserLockedOut": "Korisnik zaključan",
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
|
||||
"Photos": "Slike",
|
||||
"Playlists": "Popis za reprodukciju",
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započela",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
|
||||
"Photos": "Fotografije",
|
||||
"Playlists": "Popisi za reprodukciju",
|
||||
"Plugin": "Dodatak",
|
||||
"PluginInstalledWithName": "{0} je instalirano",
|
||||
"PluginUninstalledWithName": "{0} je deinstalirano",
|
||||
"PluginUpdatedWithName": "{0} je ažurirano",
|
||||
"ProviderValue": "Pružitelj: {0}",
|
||||
"ProviderValue": "Pružatelj: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} neuspjelo",
|
||||
"ScheduledTaskStartedWithName": "{0} pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
|
||||
"Shows": "Serije",
|
||||
"Songs": "Pjesme",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
|
||||
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
|
||||
"Sync": "Sink.",
|
||||
"System": "Sistem",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
|
||||
"Sync": "Sinkronizacija",
|
||||
"System": "Sustav",
|
||||
"TvShows": "Serije",
|
||||
"User": "Korisnik",
|
||||
"UserCreatedWithName": "Korisnik {0} je stvoren",
|
||||
"UserCreatedWithName": "Korisnik {0} je kreiran",
|
||||
"UserDeletedWithName": "Korisnik {0} je obrisan",
|
||||
"UserDownloadingItemWithValues": "{0} se preuzima {1}",
|
||||
"UserDownloadingItemWithValues": "{0} preuzima {1}",
|
||||
"UserLockedOutWithName": "Korisnik {0} je zaključan",
|
||||
"UserOfflineFromDevice": "{0} se odspojilo od {1}",
|
||||
"UserOnlineFromDevice": "{0} je online od {1}",
|
||||
"UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
|
||||
"UserOnlineFromDevice": "{0} povezan od {1}",
|
||||
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
|
||||
"ValueSpecialEpisodeName": "Specijal - {0}",
|
||||
"ValueSpecialEpisodeName": "Posebno - {0}",
|
||||
"VersionNumber": "Verzija {0}",
|
||||
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
|
||||
"TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
|
||||
"TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
|
||||
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
|
||||
"TaskCleanCache": "Očisti priručnu memoriju",
|
||||
"TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
|
||||
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
|
||||
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
|
||||
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
|
||||
"TaskCleanCache": "Očisti mapu predmemorije",
|
||||
"TasksApplicationCategory": "Aplikacija",
|
||||
"TasksMaintenanceCategory": "Održavanje",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
|
||||
"TaskRefreshChannels": "Osvježi kanale",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti direktorij za transkodiranje",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti mapu transkodiranja",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
|
||||
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
||||
"TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi ljude",
|
||||
"TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti direktorij sa logovima",
|
||||
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi osobe",
|
||||
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
|
||||
"TasksChannelsCategory": "Internet kanali",
|
||||
"TasksLibraryCategory": "Biblioteka"
|
||||
"TasksLibraryCategory": "Biblioteka",
|
||||
"TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
|
||||
"TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
|
||||
"TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
|
||||
"TaskRefreshChannels": "Csatornák frissítése",
|
||||
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat."
|
||||
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
|
||||
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
|
||||
"TaskCleanActivityLog": "Tevékenységnapló törlése"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TasksChannelsCategory": "Canali su Internet",
|
||||
"TasksApplicationCategory": "Applicazione",
|
||||
"TasksLibraryCategory": "Libreria",
|
||||
"TasksMaintenanceCategory": "Manutenzione"
|
||||
"TasksMaintenanceCategory": "Manutenzione",
|
||||
"TaskCleanActivityLog": "Attività di Registro Completate",
|
||||
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata."
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
|
||||
"TaskRefreshLibrary": "メディアライブラリのスキャン",
|
||||
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
|
||||
"TaskCleanCache": "キャッシュの掃除",
|
||||
"TaskCleanCache": "キャッシュを消去",
|
||||
"TasksChannelsCategory": "ネットチャンネル",
|
||||
"TasksApplicationCategory": "アプリケーション",
|
||||
"TasksLibraryCategory": "ライブラリ",
|
||||
@@ -112,5 +112,7 @@
|
||||
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
|
||||
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
|
||||
"TaskRefreshChapterImages": "チャプター画像を抽出する",
|
||||
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
|
||||
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
|
||||
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
|
||||
"TaskCleanActivityLog": "アクティビティの履歴を消去"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"HeaderRecordingGroups": "녹화 그룹",
|
||||
"HomeVideos": "홈 비디오",
|
||||
"Inherit": "상속",
|
||||
"ItemAddedWithName": "{0}가 라이브러리에 추가됨",
|
||||
"ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
|
||||
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
|
||||
"LabelIpAddressValue": "IP 주소: {0}",
|
||||
"LabelRunningTimeValue": "상영 시간: {0}",
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
|
||||
"TaskCleanCache": "캐시 폴더 청소",
|
||||
"TasksChannelsCategory": "인터넷 채널",
|
||||
"TasksLibraryCategory": "라이브러리"
|
||||
"TasksLibraryCategory": "라이브러리",
|
||||
"TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
|
||||
"TaskCleanActivityLog": "활동내역청소"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TasksChannelsCategory": "Internet Kanalen",
|
||||
"TasksApplicationCategory": "Applicatie",
|
||||
"TasksLibraryCategory": "Bibliotheek",
|
||||
"TasksMaintenanceCategory": "Onderhoud"
|
||||
"TasksMaintenanceCategory": "Onderhoud",
|
||||
"TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
|
||||
"TaskCleanActivityLog": "Leeg activiteiten logboek"
|
||||
}
|
||||
|
||||
@@ -112,5 +112,7 @@
|
||||
"TasksChannelsCategory": "Canale de pe Internet",
|
||||
"TasksApplicationCategory": "Aplicație",
|
||||
"TasksLibraryCategory": "Librărie",
|
||||
"TasksMaintenanceCategory": "Mentenanță"
|
||||
"TasksMaintenanceCategory": "Mentenanță",
|
||||
"TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
|
||||
"TaskCleanActivityLog": "Curăță Jurnalul de Activitate"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
|
||||
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
|
||||
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
|
||||
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
|
||||
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
|
||||
"TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
|
||||
"TaskCleanActivityLog": "Очистить журнал активности"
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
|
||||
"Application": "Aplikacija",
|
||||
"Artists": "Izvajalci",
|
||||
"AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno",
|
||||
"AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil",
|
||||
"Books": "Knjige",
|
||||
"CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}",
|
||||
"CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavje {0}",
|
||||
"Collections": "Zbirke",
|
||||
"DeviceOfflineWithName": "{0} je prekinil povezavo",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
|
||||
"Favorites": "Priljubljeno",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Zvrsti",
|
||||
"HeaderAlbumArtists": "Izvajalci albuma",
|
||||
"HeaderContinueWatching": "Nadaljuj gledanje",
|
||||
"HeaderContinueWatching": "Nadaljuj z ogledom",
|
||||
"HeaderFavoriteAlbums": "Priljubljeni albumi",
|
||||
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
|
||||
"HeaderFavoriteEpisodes": "Priljubljene epizode",
|
||||
@@ -32,23 +32,23 @@
|
||||
"LabelIpAddressValue": "IP naslov: {0}",
|
||||
"LabelRunningTimeValue": "Čas trajanja: {0}",
|
||||
"Latest": "Najnovejše",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
|
||||
"MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen",
|
||||
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
|
||||
"MixedContent": "Razne vsebine",
|
||||
"MixedContent": "Mešane vsebine",
|
||||
"Movies": "Filmi",
|
||||
"Music": "Glasba",
|
||||
"MusicVideos": "Glasbeni videi",
|
||||
"NameInstallFailed": "{0} namestitev neuspešna",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Season neznana",
|
||||
"NameSeasonUnknown": "Neznana sezona",
|
||||
"NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
|
||||
"NotificationOptionAudioPlayback": "Predvajanje zvoka začeto",
|
||||
"NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno",
|
||||
"NotificationOptionCameraImageUploaded": "Posnetek kamere naložen",
|
||||
"NotificationOptionAudioPlayback": "Predvajanje zvoka se je začelo",
|
||||
"NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka se je ustavilo",
|
||||
"NotificationOptionCameraImageUploaded": "Fotografija naložena",
|
||||
"NotificationOptionInstallationFailed": "Namestitev neuspešna",
|
||||
"NotificationOptionNewLibraryContent": "Nove vsebine dodane",
|
||||
"NotificationOptionPluginError": "Napaka dodatka",
|
||||
@@ -56,41 +56,41 @@
|
||||
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
|
||||
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
|
||||
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
|
||||
"NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
|
||||
"NotificationOptionTaskFailed": "Načrtovano opravilo neuspešno",
|
||||
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
|
||||
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
|
||||
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
|
||||
"Photos": "Fotografije",
|
||||
"Playlists": "Seznami predvajanja",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin": "Dodatek",
|
||||
"PluginInstalledWithName": "{0} je bil nameščen",
|
||||
"PluginUninstalledWithName": "{0} je bil odstranjen",
|
||||
"PluginUpdatedWithName": "{0} je bil posodobljen",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ProviderValue": "Ponudnik: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ni uspelo",
|
||||
"ScheduledTaskStartedWithName": "{0} začeto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
|
||||
"Shows": "Serije",
|
||||
"Songs": "Pesmi",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
|
||||
"Sync": "Sinhroniziraj",
|
||||
"System": "System",
|
||||
"System": "Sistem",
|
||||
"TvShows": "TV serije",
|
||||
"User": "User",
|
||||
"User": "Uporabnik",
|
||||
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
|
||||
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
|
||||
"UserDownloadingItemWithValues": "{0} prenaša {1}",
|
||||
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
|
||||
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
|
||||
"UserOnlineFromDevice": "{0} je aktiven iz {1}",
|
||||
"UserOnlineFromDevice": "{0} je aktiven na {1}",
|
||||
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
|
||||
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
|
||||
"ValueSpecialEpisodeName": "Poseben - {0}",
|
||||
"ValueSpecialEpisodeName": "Posebna - {0}",
|
||||
"VersionNumber": "Različica {0}",
|
||||
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
|
||||
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
|
||||
@@ -102,7 +102,7 @@
|
||||
"TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
|
||||
"TaskRefreshPeople": "Osveži osebe",
|
||||
"TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.",
|
||||
"TaskCleanLogs": "Počisti mapo dnevnika",
|
||||
"TaskCleanLogs": "Počisti mapo dnevnikov",
|
||||
"TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
|
||||
"TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
|
||||
"TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",
|
||||
|
||||
@@ -112,5 +112,7 @@
|
||||
"TasksChannelsCategory": "Интернет канали",
|
||||
"TasksApplicationCategory": "Апликација",
|
||||
"TasksLibraryCategory": "Библиотека",
|
||||
"TasksMaintenanceCategory": "Одржавање"
|
||||
"TasksMaintenanceCategory": "Одржавање",
|
||||
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
|
||||
"TaskCleanActivityLog": "Очисти историју активности"
|
||||
}
|
||||
|
||||
@@ -112,5 +112,7 @@
|
||||
"UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
|
||||
"HomeVideos": "முகப்பு வீடியோக்கள்",
|
||||
"UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
|
||||
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
|
||||
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது",
|
||||
"TaskCleanActivityLogDescription": "உள்ளமைக்கப்பட்ட வயதை விட பழைய செயல்பாட்டு பதிவு உள்ளீடுகளை நீக்குகிறது.",
|
||||
"TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
|
||||
"Channels": "Kanallar",
|
||||
"ChapterNameValue": "Bölüm {0}",
|
||||
"Collections": "Koleksiyonlar",
|
||||
"Collections": "Koleksiyon",
|
||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||
"DeviceOnlineWithName": "{0} bağlı",
|
||||
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
|
||||
@@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteShows": "Favori Diziler",
|
||||
"HeaderFavoriteSongs": "Favori Şarkılar",
|
||||
"HeaderLiveTV": "Canlı TV",
|
||||
"HeaderNextUp": "Sonraki hafta",
|
||||
"HeaderNextUp": "Gelecek Hafta",
|
||||
"HeaderRecordingGroups": "Kayıt Grupları",
|
||||
"HomeVideos": "Ev videoları",
|
||||
"Inherit": "Devral",
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
|
||||
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
|
||||
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
|
||||
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler."
|
||||
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
|
||||
"TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
|
||||
"TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi."
|
||||
}
|
||||
|
||||
@@ -112,5 +112,7 @@
|
||||
"Books": "Sách",
|
||||
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
|
||||
"Application": "Ứng Dụng",
|
||||
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
|
||||
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
|
||||
"TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
|
||||
"TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
|
||||
"TaskCleanCache": "清理缓存目录",
|
||||
"TasksApplicationCategory": "应用程序",
|
||||
"TasksMaintenanceCategory": "维护"
|
||||
"TasksMaintenanceCategory": "维护",
|
||||
"TaskCleanActivityLog": "清理程序日志",
|
||||
"TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
|
||||
}
|
||||
|
||||
@@ -112,5 +112,7 @@
|
||||
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
|
||||
"TasksChannelsCategory": "網路頻道",
|
||||
"TasksApplicationCategory": "應用程式",
|
||||
"TasksMaintenanceCategory": "維修"
|
||||
"TasksMaintenanceCategory": "維護",
|
||||
"TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
|
||||
"TaskCleanActivityLog": "清除活動紀錄"
|
||||
}
|
||||
|
||||
@@ -653,7 +653,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(Name + ": Waiting on Task");
|
||||
var exited = Task.WaitAll(new[] { task }, 2000);
|
||||
var exited = task.Wait(2000);
|
||||
|
||||
if (exited)
|
||||
{
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||
try
|
||||
{
|
||||
previouslyFailedImages = File.ReadAllText(failHistoryPath)
|
||||
.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Updates;
|
||||
@@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error downloading {0}", package.Name);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Net;
|
||||
@@ -55,9 +56,9 @@ namespace Emby.Server.Implementations.Session
|
||||
connection.Closed += OnConnectionClosed;
|
||||
}
|
||||
|
||||
private void OnConnectionClosed(object sender, EventArgs e)
|
||||
private void OnConnectionClosed(object? sender, EventArgs e)
|
||||
{
|
||||
var connection = (IWebSocketConnection)sender;
|
||||
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||
_sockets.Remove(connection);
|
||||
connection.Closed -= OnConnectionClosed;
|
||||
|
||||
@@ -16,7 +16,7 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Common.System;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Events.Updates;
|
||||
@@ -25,7 +25,6 @@ using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
namespace Emby.Server.Implementations.Updates
|
||||
{
|
||||
@@ -49,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
/// Gets the application host.
|
||||
/// </summary>
|
||||
/// <value>The application host.</value>
|
||||
private readonly IApplicationHost _applicationHost;
|
||||
private readonly IServerApplicationHost _applicationHost;
|
||||
|
||||
private readonly IZipClient _zipClient;
|
||||
|
||||
@@ -67,7 +66,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
public InstallationManager(
|
||||
ILogger<InstallationManager> logger,
|
||||
IApplicationHost appHost,
|
||||
IServerApplicationHost appHost,
|
||||
IApplicationPaths appPaths,
|
||||
IEventManager eventManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
@@ -117,11 +116,6 @@ namespace Emby.Server.Implementations.Updates
|
||||
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||
@@ -217,7 +211,8 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
|
||||
{
|
||||
foreach (var plugin in _applicationHost.Plugins)
|
||||
var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
|
||||
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
|
||||
|
||||
Reference in New Issue
Block a user