mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-21 17:44:43 +01:00
Merge remote-tracking branch 'upstream/master' into register-services-correctly
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -27,6 +25,9 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry point for the activity logger.
|
||||
/// </summary>
|
||||
public sealed class ActivityLogEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
@@ -42,16 +43,15 @@ namespace Emby.Server.Implementations.Activity
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="sessionManager"></param>
|
||||
/// <param name="deviceManager"></param>
|
||||
/// <param name="taskManager"></param>
|
||||
/// <param name="activityManager"></param>
|
||||
/// <param name="localization"></param>
|
||||
/// <param name="installationManager"></param>
|
||||
/// <param name="subManager"></param>
|
||||
/// <param name="userManager"></param>
|
||||
/// <param name="appHost"></param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="deviceManager">The device manager.</param>
|
||||
/// <param name="taskManager">The task manager.</param>
|
||||
/// <param name="activityManager">The activity manager.</param>
|
||||
/// <param name="localization">The localization manager.</param>
|
||||
/// <param name="installationManager">The installation manager.</param>
|
||||
/// <param name="subManager">The subtitle manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public ActivityLogEntryPoint(
|
||||
ILogger<ActivityLogEntryPoint> logger,
|
||||
ISessionManager sessionManager,
|
||||
@@ -74,6 +74,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_taskManager.TaskCompleted += OnTaskCompleted;
|
||||
@@ -168,7 +169,12 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName),
|
||||
Name = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
|
||||
user.Name,
|
||||
GetItemName(item),
|
||||
e.DeviceName),
|
||||
Type = GetPlaybackStoppedNotificationType(item.MediaType),
|
||||
UserId = user.Id
|
||||
});
|
||||
@@ -416,7 +422,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e)
|
||||
private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
@@ -428,8 +434,8 @@ namespace Emby.Server.Implementations.Activity
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
e.Argument.Item2.versionStr),
|
||||
Overview = e.Argument.Item2.description
|
||||
e.Argument.Item2.version),
|
||||
Overview = e.Argument.Item2.changelog
|
||||
});
|
||||
}
|
||||
|
||||
@@ -445,7 +451,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
|
||||
private void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
@@ -457,7 +463,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
e.Argument.versionStr)
|
||||
e.Argument.version)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -485,8 +491,8 @@ namespace Emby.Server.Implementations.Activity
|
||||
var result = e.Result;
|
||||
var task = e.Task;
|
||||
|
||||
var activityTask = task.ScheduledTask as IConfigurableScheduledTask;
|
||||
if (activityTask != null && !activityTask.IsLogged)
|
||||
if (task.ScheduledTask is IConfigurableScheduledTask activityTask
|
||||
&& !activityTask.IsLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -560,7 +566,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
/// <summary>
|
||||
/// Constructs a user-friendly string for this TimeSpan instance.
|
||||
/// </summary>
|
||||
public static string ToUserFriendlyString(TimeSpan span)
|
||||
private static string ToUserFriendlyString(TimeSpan span)
|
||||
{
|
||||
const int DaysInYear = 365;
|
||||
const int DaysInMonth = 30;
|
||||
|
||||
@@ -11,22 +11,17 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
public class ActivityManager : IActivityManager
|
||||
{
|
||||
public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
|
||||
|
||||
private readonly IActivityRepository _repo;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public ActivityManager(
|
||||
ILogger<ActivityManager> logger,
|
||||
IActivityRepository repo,
|
||||
IUserManager userManager)
|
||||
public ActivityManager(IActivityRepository repo, IUserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_repo = repo;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
|
||||
|
||||
public void Create(ActivityLogEntry entry)
|
||||
{
|
||||
entry.Date = DateTime.UtcNow;
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
public class ActivityRepository : BaseSqliteRepository, IActivityRepository
|
||||
{
|
||||
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||
private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public ActivityRepository(ILogger<ActivityRepository> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem)
|
||||
@@ -76,8 +77,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
}
|
||||
}
|
||||
|
||||
private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
|
||||
|
||||
public void Create(ActivityLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
@@ -87,32 +86,34 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"))
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
statement.TryBind("@Name", entry.Name);
|
||||
|
||||
statement.TryBind("@Overview", entry.Overview);
|
||||
statement.TryBind("@ShortOverview", entry.ShortOverview);
|
||||
statement.TryBind("@Type", entry.Type);
|
||||
statement.TryBind("@ItemId", entry.ItemId);
|
||||
|
||||
if (entry.UserId.Equals(Guid.Empty))
|
||||
using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"))
|
||||
{
|
||||
statement.TryBindNull("@UserId");
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
statement.TryBind("@Name", entry.Name);
|
||||
|
||||
statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
|
||||
statement.TryBind("@LogSeverity", entry.Severity.ToString());
|
||||
statement.TryBind("@Overview", entry.Overview);
|
||||
statement.TryBind("@ShortOverview", entry.ShortOverview);
|
||||
statement.TryBind("@Type", entry.Type);
|
||||
statement.TryBind("@ItemId", entry.ItemId);
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
}, TransactionMode);
|
||||
if (entry.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
statement.TryBindNull("@UserId");
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
|
||||
statement.TryBind("@LogSeverity", entry.Severity.ToString());
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,33 +126,35 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"))
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
statement.TryBind("@Id", entry.Id);
|
||||
|
||||
statement.TryBind("@Name", entry.Name);
|
||||
statement.TryBind("@Overview", entry.Overview);
|
||||
statement.TryBind("@ShortOverview", entry.ShortOverview);
|
||||
statement.TryBind("@Type", entry.Type);
|
||||
statement.TryBind("@ItemId", entry.ItemId);
|
||||
|
||||
if (entry.UserId.Equals(Guid.Empty))
|
||||
using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"))
|
||||
{
|
||||
statement.TryBindNull("@UserId");
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
statement.TryBind("@Id", entry.Id);
|
||||
|
||||
statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
|
||||
statement.TryBind("@LogSeverity", entry.Severity.ToString());
|
||||
statement.TryBind("@Name", entry.Name);
|
||||
statement.TryBind("@Overview", entry.Overview);
|
||||
statement.TryBind("@ShortOverview", entry.ShortOverview);
|
||||
statement.TryBind("@Type", entry.Type);
|
||||
statement.TryBind("@ItemId", entry.ItemId);
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
}, TransactionMode);
|
||||
if (entry.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
statement.TryBindNull("@UserId");
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
|
||||
statement.TryBind("@LogSeverity", entry.Severity.ToString());
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +167,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
whereClauses.Add("DateCreated>=@DateCreated");
|
||||
}
|
||||
|
||||
if (hasUserId.HasValue)
|
||||
{
|
||||
if (hasUserId.Value)
|
||||
@@ -204,7 +208,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
commandText += " LIMIT " + limit.Value.ToString(_usCulture);
|
||||
commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var statementTexts = new[]
|
||||
@@ -304,7 +308,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
index++;
|
||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
||||
{
|
||||
info.Severity = (LogLevel)Enum.Parse(typeof(LogLevel), reader[index].ToString(), true);
|
||||
info.Severity = Enum.Parse<LogLevel>(reader[index].ToString(), true);
|
||||
}
|
||||
|
||||
return info;
|
||||
|
||||
@@ -213,19 +213,6 @@ namespace Emby.Server.Implementations
|
||||
/// <value>The configuration manager.</value>
|
||||
protected IConfigurationManager ConfigurationManager { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public PackageVersionClass SystemUpdateLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BETA
|
||||
return PackageVersionClass.Beta;
|
||||
#else
|
||||
return PackageVersionClass.Release;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service provider.
|
||||
/// </summary>
|
||||
@@ -1170,7 +1157,6 @@ namespace Emby.Server.Implementations
|
||||
SupportsLibraryMonitor = true,
|
||||
EncoderLocation = _mediaEncoder.EncoderLocation,
|
||||
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
||||
SystemUpdateLevel = SystemUpdateLevel,
|
||||
PackageName = _startupOptions.PackageName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace Emby.Server.Implementations.Archiving
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using (var reader = ZipReader.Open(source))
|
||||
@@ -66,6 +67,7 @@ namespace Emby.Server.Implementations.Archiving
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using (var reader = GZipReader.Open(source))
|
||||
@@ -82,6 +84,7 @@ namespace Emby.Server.Implementations.Archiving
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName)
|
||||
{
|
||||
using (var reader = GZipReader.Open(source))
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
_channelManager = channelManager;
|
||||
}
|
||||
|
||||
public string Name => "Channel Image Provider";
|
||||
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return GetChannel(item).GetSupportedChannelImages();
|
||||
@@ -32,8 +34,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
return channel.GetChannelImage(type, cancellationToken);
|
||||
}
|
||||
|
||||
public string Name => "Channel Image Provider";
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Channel;
|
||||
|
||||
@@ -31,8 +31,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
public class ChannelManager : IChannelManager
|
||||
{
|
||||
internal IChannel[] Channels { get; private set; }
|
||||
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
@@ -43,11 +41,16 @@ namespace Emby.Server.Implementations.Channels
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
|
||||
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
|
||||
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
|
||||
public ChannelManager(
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService,
|
||||
ILibraryManager libraryManager,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<ChannelManager> logger,
|
||||
IServerConfigurationManager config,
|
||||
IFileSystem fileSystem,
|
||||
IUserDataManager userDataManager,
|
||||
@@ -57,7 +60,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = loggerFactory.CreateLogger(nameof(ChannelManager));
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_userDataManager = userDataManager;
|
||||
@@ -65,6 +68,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
internal IChannel[] Channels { get; private set; }
|
||||
|
||||
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
|
||||
|
||||
public void AddParts(IEnumerable<IChannel> channels)
|
||||
@@ -85,8 +90,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||
|
||||
var supportsDelete = channel as ISupportsDelete;
|
||||
return supportsDelete != null && supportsDelete.CanDelete(item);
|
||||
return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item);
|
||||
}
|
||||
|
||||
public bool EnableMediaProbe(BaseItem item)
|
||||
@@ -146,15 +150,13 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
try
|
||||
{
|
||||
var hasAttributes = GetChannelProvider(i) as IHasFolderAttributes;
|
||||
|
||||
return (hasAttributes != null && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
|
||||
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@@ -171,7 +173,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@@ -188,9 +189,9 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
if (query.IsFavorite.HasValue)
|
||||
{
|
||||
var val = query.IsFavorite.Value;
|
||||
@@ -215,7 +216,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
all = all.Skip(query.StartIndex.Value).ToList();
|
||||
}
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
all = all.Take(query.Limit.Value).ToList();
|
||||
@@ -256,11 +257,9 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var internalResult = GetChannelsInternal(query);
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
{
|
||||
};
|
||||
var dtoOptions = new DtoOptions();
|
||||
|
||||
//TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
|
||||
// TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
|
||||
var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
@@ -341,8 +340,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -365,11 +364,9 @@ namespace Emby.Server.Implementations.Channels
|
||||
var channel = GetChannel(item.ChannelId);
|
||||
var channelPlugin = GetChannelProvider(channel);
|
||||
|
||||
var requiresCallback = channelPlugin as IRequiresMediaInfoCallback;
|
||||
|
||||
IEnumerable<MediaSourceInfo> results;
|
||||
|
||||
if (requiresCallback != null)
|
||||
if (channelPlugin is IRequiresMediaInfoCallback requiresCallback)
|
||||
{
|
||||
results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@@ -384,9 +381,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
|
||||
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
|
||||
|
||||
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
|
||||
@@ -444,18 +438,21 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
item.Path = path;
|
||||
|
||||
if (!item.ChannelId.Equals(id))
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.ChannelId = id;
|
||||
|
||||
if (item.ParentId != parentFolderId)
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.ParentId = parentFolderId;
|
||||
|
||||
item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
|
||||
@@ -472,10 +469,12 @@ namespace Emby.Server.Implementations.Channels
|
||||
_libraryManager.CreateItem(item, null);
|
||||
}
|
||||
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ForceSave = !isNew && forceUpdate
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ForceSave = !isNew && forceUpdate
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -509,12 +508,12 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
public ChannelFeatures[] GetAllChannelFeatures()
|
||||
{
|
||||
return _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Channel).Name },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||
|
||||
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
|
||||
return _libraryManager.GetItemIds(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Channel).Name },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
|
||||
}
|
||||
|
||||
public ChannelFeatures GetChannelFeatures(string id)
|
||||
@@ -532,13 +531,13 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
public bool SupportsExternalTransfer(Guid channelId)
|
||||
{
|
||||
//var channel = GetChannel(channelId);
|
||||
var channelProvider = GetChannelProvider(channelId);
|
||||
|
||||
return channelProvider.GetChannelFeatures().SupportsContentDownloading;
|
||||
}
|
||||
|
||||
public ChannelFeatures GetChannelFeaturesDto(Channel channel,
|
||||
public ChannelFeatures GetChannelFeaturesDto(
|
||||
Channel channel,
|
||||
IChannel provider,
|
||||
InternalChannelFeatures features)
|
||||
{
|
||||
@@ -567,6 +566,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
|
||||
}
|
||||
|
||||
@@ -614,7 +614,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
query.IsFolder = false;
|
||||
|
||||
// hack for trailers, figure out a better way later
|
||||
var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.IndexOf("Trailer") != -1;
|
||||
var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal);
|
||||
|
||||
if (sortByPremiereDate)
|
||||
{
|
||||
@@ -640,10 +640,12 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var query = new InternalItemsQuery();
|
||||
query.Parent = internalChannel;
|
||||
query.EnableTotalRecordCount = false;
|
||||
query.ChannelIds = new Guid[] { internalChannel.Id };
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
Parent = internalChannel,
|
||||
EnableTotalRecordCount = false,
|
||||
ChannelIds = new Guid[] { internalChannel.Id }
|
||||
};
|
||||
|
||||
var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -651,13 +653,15 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
if (item is Folder folder)
|
||||
{
|
||||
await GetChannelItemsInternal(new InternalItemsQuery
|
||||
{
|
||||
Parent = folder,
|
||||
EnableTotalRecordCount = false,
|
||||
ChannelIds = new Guid[] { internalChannel.Id }
|
||||
|
||||
}, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
|
||||
await GetChannelItemsInternal(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
Parent = folder,
|
||||
EnableTotalRecordCount = false,
|
||||
ChannelIds = new Guid[] { internalChannel.Id }
|
||||
},
|
||||
new SimpleProgress<double>(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,7 +676,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId);
|
||||
|
||||
var itemsResult = await GetChannelItems(channelProvider,
|
||||
var itemsResult = await GetChannelItems(
|
||||
channelProvider,
|
||||
query.User,
|
||||
parentItem is Channel ? null : parentItem.ExternalId,
|
||||
null,
|
||||
@@ -684,13 +689,12 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
query.Parent = channel;
|
||||
}
|
||||
|
||||
query.ChannelIds = Array.Empty<Guid>();
|
||||
|
||||
// Not yet sure why this is causing a problem
|
||||
query.GroupByPresentationUniqueKey = false;
|
||||
|
||||
//_logger.LogDebug("GetChannelItemsInternal");
|
||||
|
||||
// null if came from cache
|
||||
if (itemsResult != null)
|
||||
{
|
||||
@@ -707,12 +711,15 @@ namespace Emby.Server.Implementations.Channels
|
||||
var deadItem = _libraryManager.GetItemById(deadId);
|
||||
if (deadItem != null)
|
||||
{
|
||||
_libraryManager.DeleteItem(deadItem, new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false,
|
||||
DeleteFromExternalProvider = false
|
||||
|
||||
}, parentItem, false);
|
||||
_libraryManager.DeleteItem(
|
||||
deadItem,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false,
|
||||
DeleteFromExternalProvider = false
|
||||
},
|
||||
parentItem,
|
||||
false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,7 +742,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
return result;
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
|
||||
User user,
|
||||
string externalFolderId,
|
||||
@@ -743,7 +749,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
bool sortDescending,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = user == null ? null : user.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
var cacheLength = CacheLength;
|
||||
var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
|
||||
@@ -761,11 +767,9 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -785,11 +789,9 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
var query = new InternalChannelItemQuery
|
||||
@@ -833,7 +835,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
}
|
||||
|
||||
private string GetChannelDataCachePath(IChannel channel,
|
||||
private string GetChannelDataCachePath(
|
||||
IChannel channel,
|
||||
string userId,
|
||||
string externalFolderId,
|
||||
ChannelItemSortField? sortField,
|
||||
@@ -843,8 +846,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var userCacheKey = string.Empty;
|
||||
|
||||
var hasCacheKey = channel as IHasCacheKey;
|
||||
if (hasCacheKey != null)
|
||||
if (channel is IHasCacheKey hasCacheKey)
|
||||
{
|
||||
userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty;
|
||||
}
|
||||
@@ -858,6 +860,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
filename += "-sortField-" + sortField.Value;
|
||||
}
|
||||
|
||||
if (sortDescending)
|
||||
{
|
||||
filename += "-sortDescending";
|
||||
@@ -865,7 +868,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
return Path.Combine(_config.ApplicationPaths.CachePath,
|
||||
return Path.Combine(
|
||||
_config.ApplicationPaths.CachePath,
|
||||
"channels",
|
||||
channelId,
|
||||
version,
|
||||
@@ -981,7 +985,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
item.RunTimeTicks = null;
|
||||
}
|
||||
|
||||
else if (isNew || !enableMediaProbe)
|
||||
{
|
||||
item.RunTimeTicks = info.RunTimeTicks;
|
||||
@@ -1014,26 +1017,24 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
}
|
||||
|
||||
var hasArtists = item as IHasArtist;
|
||||
if (hasArtists != null)
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = info.Artists.ToArray();
|
||||
}
|
||||
|
||||
var hasAlbumArtists = item as IHasAlbumArtist;
|
||||
if (hasAlbumArtists != null)
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
|
||||
}
|
||||
|
||||
var trailer = item as Trailer;
|
||||
if (trailer != null)
|
||||
if (item is Trailer trailer)
|
||||
{
|
||||
if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
|
||||
{
|
||||
_logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
trailer.TrailerTypes = info.TrailerTypes.ToArray();
|
||||
}
|
||||
|
||||
@@ -1057,6 +1058,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
forceUpdate = true;
|
||||
_logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
|
||||
}
|
||||
|
||||
item.ChannelId = internalChannelId;
|
||||
|
||||
if (!item.ParentId.Equals(parentFolderId))
|
||||
@@ -1064,16 +1066,17 @@ namespace Emby.Server.Implementations.Channels
|
||||
forceUpdate = true;
|
||||
_logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
|
||||
}
|
||||
|
||||
item.ParentId = parentFolderId;
|
||||
|
||||
var hasSeries = item as IHasSeries;
|
||||
if (hasSeries != null)
|
||||
if (item is IHasSeries hasSeries)
|
||||
{
|
||||
if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
forceUpdate = true;
|
||||
_logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
|
||||
}
|
||||
|
||||
hasSeries.SeriesName = info.SeriesName;
|
||||
}
|
||||
|
||||
@@ -1082,24 +1085,23 @@ namespace Emby.Server.Implementations.Channels
|
||||
forceUpdate = true;
|
||||
_logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
|
||||
}
|
||||
|
||||
item.ExternalId = info.Id;
|
||||
|
||||
var channelAudioItem = item as Audio;
|
||||
if (channelAudioItem != null)
|
||||
if (item is Audio channelAudioItem)
|
||||
{
|
||||
channelAudioItem.ExtraType = info.ExtraType;
|
||||
|
||||
var mediaSource = info.MediaSources.FirstOrDefault();
|
||||
item.Path = mediaSource == null ? null : mediaSource.Path;
|
||||
item.Path = mediaSource?.Path;
|
||||
}
|
||||
|
||||
var channelVideoItem = item as Video;
|
||||
if (channelVideoItem != null)
|
||||
if (item is Video channelVideoItem)
|
||||
{
|
||||
channelVideoItem.ExtraType = info.ExtraType;
|
||||
|
||||
var mediaSource = info.MediaSources.FirstOrDefault();
|
||||
item.Path = mediaSource == null ? null : mediaSource.Path;
|
||||
item.Path = mediaSource?.Path;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
|
||||
@@ -1156,7 +1158,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime))
|
||||
if (isNew || forceUpdate || item.DateLastRefreshed == default)
|
||||
{
|
||||
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,12 @@ namespace Emby.Server.Implementations.Channels
|
||||
public class ChannelPostScanTask
|
||||
{
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager)
|
||||
public ChannelPostScanTask(IChannelManager channelManager, ILogger logger, ILibraryManager libraryManager)
|
||||
{
|
||||
_channelManager = channelManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
@@ -7,29 +7,26 @@ using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
public RefreshChannelsScheduledTask(
|
||||
IChannelManager channelManager,
|
||||
IUserManager userManager,
|
||||
ILogger<RefreshChannelsScheduledTask> logger,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localization)
|
||||
{
|
||||
_channelManager = channelManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
@@ -63,7 +60,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await new ChannelPostScanTask(_channelManager, _userManager, _logger, _libraryManager).Run(progress, cancellationToken)
|
||||
await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -72,7 +69,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
|
||||
// Every so often
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
|
||||
@@ -46,9 +46,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var subItem = i;
|
||||
|
||||
var episode = subItem as Episode;
|
||||
|
||||
if (episode != null)
|
||||
if (subItem is Episode episode)
|
||||
{
|
||||
var series = episode.Series;
|
||||
if (series != null && series.HasImage(ImageType.Primary))
|
||||
|
||||
@@ -52,7 +52,9 @@ namespace Emby.Server.Implementations.Collections
|
||||
}
|
||||
|
||||
public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
|
||||
|
||||
public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
|
||||
|
||||
public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
|
||||
|
||||
private IEnumerable<Folder> FindFolders(string path)
|
||||
@@ -109,9 +111,9 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var folder = GetCollectionsFolder(false).Result;
|
||||
|
||||
return folder == null ?
|
||||
new List<BoxSet>() :
|
||||
folder.GetChildren(user, true).OfType<BoxSet>();
|
||||
return folder == null
|
||||
? Enumerable.Empty<BoxSet>()
|
||||
: folder.GetChildren(user, true).OfType<BoxSet>();
|
||||
}
|
||||
|
||||
public BoxSet CreateCollection(CollectionCreationOptions options)
|
||||
@@ -191,7 +193,6 @@ namespace Emby.Server.Implementations.Collections
|
||||
private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
|
||||
{
|
||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
||||
|
||||
if (collection == null)
|
||||
{
|
||||
throw new ArgumentException("No collection exists with the supplied Id");
|
||||
@@ -289,10 +290,13 @@ namespace Emby.Server.Implementations.Collections
|
||||
}
|
||||
|
||||
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
|
||||
_providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ForceSave = true
|
||||
}, RefreshPriority.High);
|
||||
_providerManager.QueueRefresh(
|
||||
collection.Id,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ForceSave = true
|
||||
},
|
||||
RefreshPriority.High);
|
||||
|
||||
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
{ HostWebClientKey, bool.TrueString },
|
||||
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
|
||||
{ InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest.json" },
|
||||
{ InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" },
|
||||
{ FfmpegProbeSizeKey, "1G" },
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.TrueString }
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
@@ -109,25 +107,6 @@ namespace Emby.Server.Implementations.Data
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes to bytes.
|
||||
/// </summary>
|
||||
/// <returns>System.Byte[][].</returns>
|
||||
/// <exception cref="ArgumentNullException">obj</exception>
|
||||
public static byte[] SerializeToBytes(this IJsonSerializer json, object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
json.SerializeToStream(obj, stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
|
||||
{
|
||||
var commandText = string.Format(
|
||||
@@ -383,11 +362,11 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This)
|
||||
public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
|
||||
{
|
||||
while (This.MoveNext())
|
||||
while (statement.MoveNext())
|
||||
{
|
||||
yield return This.Current;
|
||||
yield return statement.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.0" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.25.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
@@ -26,10 +27,10 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private readonly object _createdRulesLock = new object();
|
||||
private List<IPEndPoint> _createdRules = new List<IPEndPoint>();
|
||||
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
||||
|
||||
private Timer _timer;
|
||||
private string _lastConfigIdentifier;
|
||||
private string _configIdentifier;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
@@ -60,6 +61,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
return new StringBuilder(32)
|
||||
.Append(config.EnableUPnP).Append(Separator)
|
||||
.Append(config.PublicPort).Append(Separator)
|
||||
.Append(config.PublicHttpsPort).Append(Separator)
|
||||
.Append(_appHost.HttpPort).Append(Separator)
|
||||
.Append(_appHost.HttpsPort).Append(Separator)
|
||||
.Append(_appHost.EnableHttps).Append(Separator)
|
||||
@@ -69,7 +71,10 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
private void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase))
|
||||
var oldConfigIdentifier = _configIdentifier;
|
||||
_configIdentifier = GetConfigIdentifier();
|
||||
|
||||
if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Stop();
|
||||
Start();
|
||||
@@ -93,21 +98,19 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Starting NAT discovery");
|
||||
_logger.LogInformation("Starting NAT discovery");
|
||||
|
||||
NatUtility.DeviceFound += OnNatUtilityDeviceFound;
|
||||
NatUtility.StartDiscovery();
|
||||
|
||||
_timer = new Timer(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
||||
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
||||
|
||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
||||
|
||||
_lastConfigIdentifier = GetConfigIdentifier();
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
_logger.LogDebug("Stopping NAT discovery");
|
||||
_logger.LogInformation("Stopping NAT discovery");
|
||||
|
||||
NatUtility.StopDiscovery();
|
||||
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
||||
@@ -117,26 +120,16 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
||||
}
|
||||
|
||||
private void ClearCreatedRules(object state)
|
||||
{
|
||||
lock (_createdRulesLock)
|
||||
{
|
||||
_createdRules.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
|
||||
}
|
||||
|
||||
private void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
||||
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var device = e.Device;
|
||||
|
||||
CreateRules(device);
|
||||
await CreateRules(e.Device).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -144,7 +137,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
}
|
||||
|
||||
private async void CreateRules(INatDevice device)
|
||||
private Task CreateRules(INatDevice device)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
@@ -153,50 +146,46 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
// On some systems the device discovered event seems to fire repeatedly
|
||||
// This check will help ensure we're not trying to port map the same device over and over
|
||||
var address = device.DeviceEndpoint;
|
||||
|
||||
lock (_createdRulesLock)
|
||||
if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
|
||||
{
|
||||
if (!_createdRules.Contains(address))
|
||||
{
|
||||
_createdRules.Add(address);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating http port map");
|
||||
return;
|
||||
}
|
||||
return Task.WhenAll(CreatePortMaps(device));
|
||||
}
|
||||
|
||||
try
|
||||
private IEnumerable<Task> CreatePortMaps(INatDevice device)
|
||||
{
|
||||
yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
|
||||
|
||||
if (_appHost.EnableHttps)
|
||||
{
|
||||
await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating https port map");
|
||||
yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<Mapping> CreatePortMap(INatDevice device, int privatePort, int publicPort)
|
||||
private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Creating port map on local port {0} to public port {1} with device {2}",
|
||||
"Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
|
||||
privatePort,
|
||||
publicPort,
|
||||
device.DeviceEndpoint);
|
||||
|
||||
return device.CreatePortMapAsync(
|
||||
new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name));
|
||||
try
|
||||
{
|
||||
var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
|
||||
await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
|
||||
privatePort,
|
||||
publicPort,
|
||||
device.DeviceEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -28,6 +28,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
public class HttpResultFactory : IHttpResultFactory
|
||||
{
|
||||
// Last-Modified and If-Modified-Since must follow strict date format,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
||||
private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
|
||||
// We specifically use en-US culture because both day of week and month names require it
|
||||
private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
@@ -420,7 +426,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (!noCache)
|
||||
{
|
||||
DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
|
||||
if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal, out var ifModifiedSinceHeader))
|
||||
{
|
||||
_logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
|
||||
{
|
||||
@@ -629,7 +639,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (lastModifiedDate.HasValue)
|
||||
{
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2335,7 +2335,7 @@ namespace Emby.Server.Implementations.Library
|
||||
string videoPath,
|
||||
string[] files)
|
||||
{
|
||||
new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
|
||||
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library
|
||||
// for imdbid we also accept pattern matching
|
||||
if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase);
|
||||
var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
|
||||
return m.Success ? m.Value : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
_logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
|
||||
throw new ArgumentNullException(nameof(username));
|
||||
}
|
||||
|
||||
@@ -313,11 +314,13 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint);
|
||||
throw new AuthenticationException("Invalid username or password entered.");
|
||||
}
|
||||
|
||||
if (user.Policy.IsDisabled)
|
||||
{
|
||||
_logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint);
|
||||
throw new AuthenticationException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@@ -327,11 +330,13 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
|
||||
{
|
||||
_logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint);
|
||||
throw new AuthenticationException("Forbidden.");
|
||||
}
|
||||
|
||||
if (!user.IsParentalScheduleAllowed())
|
||||
{
|
||||
_logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint);
|
||||
throw new AuthenticationException("User is not allowed access at this time.");
|
||||
}
|
||||
|
||||
@@ -345,14 +350,14 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
ResetInvalidLoginAttemptCount(user);
|
||||
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
IncrementInvalidLoginAttemptCount(user);
|
||||
_logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied");
|
||||
|
||||
return success ? user : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
public class LiveTvDtoService
|
||||
{
|
||||
private const string InternalVersionNumber = "4";
|
||||
|
||||
private const string ServiceName = "Emby";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IDtoService _dtoService;
|
||||
@@ -160,7 +164,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Thumb },
|
||||
DtoOptions = new DtoOptions(false)
|
||||
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (librarySeries != null)
|
||||
@@ -178,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
_logger.LogError(ex, "Error");
|
||||
}
|
||||
}
|
||||
|
||||
image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
|
||||
if (image != null)
|
||||
{
|
||||
@@ -198,13 +202,12 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
ExternalSeriesId = programSeriesId,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
DtoOptions = new DtoOptions(false),
|
||||
Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
|
||||
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (program != null)
|
||||
@@ -231,9 +234,10 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
try
|
||||
{
|
||||
dto.ParentBackdropImageTags = new string[]
|
||||
{
|
||||
{
|
||||
_imageProcessor.GetImageCacheTag(program, image)
|
||||
};
|
||||
};
|
||||
|
||||
dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -254,7 +258,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Thumb },
|
||||
DtoOptions = new DtoOptions(false)
|
||||
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (librarySeries != null)
|
||||
@@ -272,6 +275,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
_logger.LogError(ex, "Error");
|
||||
}
|
||||
}
|
||||
|
||||
image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
|
||||
if (image != null)
|
||||
{
|
||||
@@ -297,7 +301,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
DtoOptions = new DtoOptions(false)
|
||||
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (program == null)
|
||||
@@ -310,7 +313,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
DtoOptions = new DtoOptions(false),
|
||||
Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
|
||||
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
|
||||
@@ -395,8 +397,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return null;
|
||||
}
|
||||
|
||||
private const string InternalVersionNumber = "4";
|
||||
|
||||
public Guid GetInternalChannelId(string serviceName, string externalId)
|
||||
{
|
||||
var name = serviceName + externalId + InternalVersionNumber;
|
||||
@@ -404,7 +404,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel));
|
||||
}
|
||||
|
||||
private const string ServiceName = "Emby";
|
||||
public string GetInternalTimerId(string externalId)
|
||||
{
|
||||
var name = ServiceName + externalId + InternalVersionNumber;
|
||||
|
||||
@@ -41,6 +41,10 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
/// </summary>
|
||||
public class LiveTvManager : ILiveTvManager, IDisposable
|
||||
{
|
||||
private const string ExternalServiceTag = "ExternalServiceId";
|
||||
|
||||
private const string EtagKey = "ProgramEtag";
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
@@ -173,7 +177,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
Name = i.Name,
|
||||
Id = i.Type
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@@ -256,6 +259,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
var endTime = DateTime.UtcNow;
|
||||
_logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
|
||||
}
|
||||
|
||||
info.RequiresClosing = true;
|
||||
|
||||
var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_";
|
||||
@@ -357,30 +361,37 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
stream.BitRate = null;
|
||||
}
|
||||
|
||||
if (stream.Channels.HasValue && stream.Channels <= 0)
|
||||
{
|
||||
stream.Channels = null;
|
||||
}
|
||||
|
||||
if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
|
||||
{
|
||||
stream.AverageFrameRate = null;
|
||||
}
|
||||
|
||||
if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
|
||||
{
|
||||
stream.RealFrameRate = null;
|
||||
}
|
||||
|
||||
if (stream.Width.HasValue && stream.Width <= 0)
|
||||
{
|
||||
stream.Width = null;
|
||||
}
|
||||
|
||||
if (stream.Height.HasValue && stream.Height <= 0)
|
||||
{
|
||||
stream.Height = null;
|
||||
}
|
||||
|
||||
if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
|
||||
{
|
||||
stream.SampleRate = null;
|
||||
}
|
||||
|
||||
if (stream.Level.HasValue && stream.Level <= 0)
|
||||
{
|
||||
stream.Level = null;
|
||||
@@ -422,7 +433,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
private const string ExternalServiceTag = "ExternalServiceId";
|
||||
private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
|
||||
{
|
||||
var parentFolderId = parentFolder.Id;
|
||||
@@ -451,6 +461,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
item.Tags = channelInfo.Tags;
|
||||
}
|
||||
|
||||
@@ -458,6 +469,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
item.ParentId = parentFolderId;
|
||||
|
||||
item.ChannelType = channelInfo.ChannelType;
|
||||
@@ -467,24 +479,28 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.SetProviderId(ExternalServiceTag, serviceName);
|
||||
|
||||
if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.ExternalId = channelInfo.Id;
|
||||
|
||||
if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.Number = channelInfo.Number;
|
||||
|
||||
if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
item.Name = channelInfo.Name;
|
||||
|
||||
if (!item.HasImage(ImageType.Primary))
|
||||
@@ -513,8 +529,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return item;
|
||||
}
|
||||
|
||||
private const string EtagKey = "ProgramEtag";
|
||||
|
||||
private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = _tvDtoService.GetInternalProgramId(info.Id);
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
"AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}",
|
||||
"Application": "Aplicació",
|
||||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament",
|
||||
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
|
||||
"Books": "Llibres",
|
||||
"CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}",
|
||||
"CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
|
||||
"Channels": "Canals",
|
||||
"ChapterNameValue": "Episodi {0}",
|
||||
"ChapterNameValue": "Capítol {0}",
|
||||
"Collections": "Col·leccions",
|
||||
"DeviceOfflineWithName": "{0} s'ha desconnectat",
|
||||
"DeviceOnlineWithName": "{0} està connectat",
|
||||
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
|
||||
"Favorites": "Preferits",
|
||||
"Folders": "Directoris",
|
||||
"Folders": "Carpetes",
|
||||
"Genres": "Gèneres",
|
||||
"HeaderAlbumArtists": "Artistes dels Àlbums",
|
||||
"HeaderAlbumArtists": "Artistes del Àlbum",
|
||||
"HeaderCameraUploads": "Pujades de Càmera",
|
||||
"HeaderContinueWatching": "Continua Veient",
|
||||
"HeaderFavoriteAlbums": "Àlbums Preferits",
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"Latest": "Seneste",
|
||||
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurationsafsnit {0} er blevet opdateret",
|
||||
"MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
|
||||
"MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
|
||||
"MixedContent": "Blandet indhold",
|
||||
"Movies": "Film",
|
||||
"Music": "Musik",
|
||||
@@ -93,13 +93,13 @@
|
||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiration.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.",
|
||||
"TaskDownloadMissingSubtitles": "Download manglende undertekster",
|
||||
"TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.",
|
||||
"TaskUpdatePlugins": "Opdater Plugins",
|
||||
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.",
|
||||
"TaskCleanLogs": "Ryd Log Mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdatere metadata.",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.",
|
||||
"TaskRefreshLibrary": "Scan Medie Bibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.",
|
||||
"TaskCleanCache": "Ryd Cache Mappe",
|
||||
@@ -108,5 +108,11 @@
|
||||
"TasksLibraryCategory": "Bibliotek",
|
||||
"TasksMaintenanceCategory": "Vedligeholdelse",
|
||||
"TaskRefreshChapterImages": "Udtræk Kapitel billeder",
|
||||
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler."
|
||||
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.",
|
||||
"TaskRefreshChannels": "Genopfrisk Kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.",
|
||||
"TaskCleanTranscode": "Rengør Transcode Mappen",
|
||||
"TaskRefreshPeople": "Genopfrisk Personer",
|
||||
"TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek."
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
||||
"Application": "Anwendung",
|
||||
"Artists": "Interpreten",
|
||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifziert",
|
||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
|
||||
"Books": "Bücher",
|
||||
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
|
||||
"Channels": "Kanäle",
|
||||
@@ -99,11 +99,11 @@
|
||||
"TaskRefreshChannels": "Erneuere Kanäle",
|
||||
"TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
|
||||
"TaskCleanTranscode": "Lösche Transkodier Pfad",
|
||||
"TaskUpdatePluginsDescription": "Läd Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
||||
"TaskUpdatePlugins": "Update Plugins",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schausteller",
|
||||
"TaskCleanLogsDescription": "Lösche Log Datein die älter als {0} Tage sind.",
|
||||
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
|
||||
"TaskCleanLogs": "Lösche Log Pfad",
|
||||
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
|
||||
"TaskRefreshLibrary": "Scanne alle Bibliotheken",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas de álbum",
|
||||
"HeaderCameraUploads": "Subidas de cámara",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderContinueWatching": "Seguir viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||
"DeviceOnlineWithName": "{0} est connecté",
|
||||
"FailedLoginAttemptWithUserName": "Échec de connexion de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}",
|
||||
"Favorites": "Favoris",
|
||||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artistes d'album",
|
||||
"HeaderAlbumArtists": "Artistes de l'album",
|
||||
"HeaderCameraUploads": "Photos transférées",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
@@ -69,7 +69,7 @@
|
||||
"PluginUpdatedWithName": "{0} a été mis à jour",
|
||||
"ProviderValue": "Fournisseur : {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} a échoué",
|
||||
"ScheduledTaskStartedWithName": "{0} a commencé",
|
||||
"ScheduledTaskStartedWithName": "{0} a démarré",
|
||||
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
|
||||
"Shows": "Émissions",
|
||||
"Songs": "Chansons",
|
||||
@@ -95,21 +95,21 @@
|
||||
"VersionNumber": "Version {0}",
|
||||
"TasksChannelsCategory": "Chaines en ligne",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.",
|
||||
"TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant",
|
||||
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.",
|
||||
"TaskRefreshChannels": "Rafraîchit les chaines",
|
||||
"TaskRefreshChannels": "Rafraîchir les chaines",
|
||||
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
|
||||
"TaskCleanTranscode": "Nettoie les dossier des transcodages",
|
||||
"TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.",
|
||||
"TaskUpdatePlugins": "Mettre à jour les plugins",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.",
|
||||
"TaskRefreshPeople": "Rafraîchit les acteurs",
|
||||
"TaskCleanTranscode": "Nettoyer les dossier des transcodages",
|
||||
"TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.",
|
||||
"TaskUpdatePlugins": "Mettre à jour les extensions",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
|
||||
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
|
||||
"TaskCleanLogs": "Nettoie le répertoire des journaux",
|
||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshLibrary": "Scanne toute les Bibliothèques",
|
||||
"TaskRefreshLibrary": "Scanner toute les Bibliothèques",
|
||||
"TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
|
||||
"TaskRefreshChapterImages": "Extrait les images de chapitre",
|
||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
|
||||
"TaskCleanCache": "Vider le répertoire cache",
|
||||
"TasksApplicationCategory": "Application",
|
||||
|
||||
@@ -109,5 +109,8 @@
|
||||
"TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
|
||||
"TaskUpdatePlugins": "プラグインの更新",
|
||||
"TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータをリフレッシュします。",
|
||||
"TaskRefreshPeople": "俳優や監督のデータのリフレッシュ"
|
||||
"TaskRefreshPeople": "俳優や監督のデータのリフレッシュ",
|
||||
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
|
||||
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
|
||||
"TaskRefreshChapterImages": "チャプター画像を抽出する"
|
||||
}
|
||||
|
||||
@@ -95,5 +95,13 @@
|
||||
"TaskCleanCache": "Limpar Diretório de Cache",
|
||||
"TasksApplicationCategory": "Aplicação",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Manutenção"
|
||||
"TasksMaintenanceCategory": "Manutenção",
|
||||
"TaskRefreshChannels": "Atualizar Canais",
|
||||
"TaskUpdatePlugins": "Atualizar Plugins",
|
||||
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar diretório de log",
|
||||
"TaskRefreshLibrary": "Escanear biblioteca de mídias",
|
||||
"TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.",
|
||||
"TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.",
|
||||
"TasksChannelsCategory": "Canais de Internet"
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"Channels": "Каналы",
|
||||
"ChapterNameValue": "Сцена {0}",
|
||||
"Collections": "Коллекции",
|
||||
"DeviceOfflineWithName": "{0} - подкл. разъ-но",
|
||||
"DeviceOnlineWithName": "{0} - подкл. уст-но",
|
||||
"DeviceOfflineWithName": "{0} - отключено",
|
||||
"DeviceOnlineWithName": "{0} - подключено",
|
||||
"FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
|
||||
"Favorites": "Избранное",
|
||||
"Folders": "Папки",
|
||||
@@ -26,30 +26,30 @@
|
||||
"HeaderLiveTV": "Эфир",
|
||||
"HeaderNextUp": "Очередное",
|
||||
"HeaderRecordingGroups": "Группы записей",
|
||||
"HomeVideos": "Дом. видео",
|
||||
"HomeVideos": "Домашнее видео",
|
||||
"Inherit": "Наследуемое",
|
||||
"ItemAddedWithName": "{0} - добавлено в медиатеку",
|
||||
"ItemRemovedWithName": "{0} - изъято из медиатеки",
|
||||
"LabelIpAddressValue": "IP-адрес: {0}",
|
||||
"LabelRunningTimeValue": "Длительность: {0}",
|
||||
"Latest": "Новейшее",
|
||||
"Latest": "Последнее",
|
||||
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Конфиг-ия сервера (раздел {0}) была обновлена",
|
||||
"MessageServerConfigurationUpdated": "Конфиг-ия сервера была обновлена",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
|
||||
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
|
||||
"MixedContent": "Смешанное содержимое",
|
||||
"Movies": "Кино",
|
||||
"Music": "Музыка",
|
||||
"MusicVideos": "Муз. видео",
|
||||
"MusicVideos": "Музыкальные клипы",
|
||||
"NameInstallFailed": "Установка {0} неудачна",
|
||||
"NameSeasonNumber": "Сезон {0}",
|
||||
"NameSeasonUnknown": "Сезон неопознан",
|
||||
"NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено",
|
||||
"NotificationOptionAudioPlayback": "Воспр-ие аудио зап-но",
|
||||
"NotificationOptionAudioPlaybackStopped": "Восп-ие аудио ост-но",
|
||||
"NotificationOptionCameraImageUploaded": "Произведена выкладка отснятого с камеры",
|
||||
"NotificationOptionAudioPlayback": "Воспроизведение аудио запущено",
|
||||
"NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено",
|
||||
"NotificationOptionCameraImageUploaded": "Изображения с камеры загружены",
|
||||
"NotificationOptionInstallationFailed": "Сбой установки",
|
||||
"NotificationOptionNewLibraryContent": "Новое содержание добавлено",
|
||||
"NotificationOptionPluginError": "Сбой плагина",
|
||||
@@ -59,8 +59,8 @@
|
||||
"NotificationOptionServerRestartRequired": "Требуется перезапуск сервера",
|
||||
"NotificationOptionTaskFailed": "Сбой назначенной задачи",
|
||||
"NotificationOptionUserLockedOut": "Пользователь заблокирован",
|
||||
"NotificationOptionVideoPlayback": "Воспр-ие видео зап-но",
|
||||
"NotificationOptionVideoPlaybackStopped": "Восп-ие видео ост-но",
|
||||
"NotificationOptionVideoPlayback": "Воспроизведение видео запущено",
|
||||
"NotificationOptionVideoPlaybackStopped": "Воспроизведение видео остановлено",
|
||||
"Photos": "Фото",
|
||||
"Playlists": "Плей-листы",
|
||||
"Plugin": "Плагин",
|
||||
@@ -76,21 +76,43 @@
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
|
||||
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
|
||||
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
|
||||
"Sync": "Синхро",
|
||||
"Sync": "Синхронизация",
|
||||
"System": "Система",
|
||||
"TvShows": "ТВ",
|
||||
"User": "Польз-ль",
|
||||
"User": "Пользователь",
|
||||
"UserCreatedWithName": "Пользователь {0} был создан",
|
||||
"UserDeletedWithName": "Пользователь {0} был удалён",
|
||||
"UserDownloadingItemWithValues": "{0} загружает {1}",
|
||||
"UserLockedOutWithName": "Пользователь {0} был заблокирован",
|
||||
"UserOfflineFromDevice": "{0} - подкл. с {1} разъ-но",
|
||||
"UserOnlineFromDevice": "{0} - подкл. с {1} уст-но",
|
||||
"UserPasswordChangedWithName": "Пароль польз-ля {0} был изменён",
|
||||
"UserPolicyUpdatedWithName": "Польз-ие политики {0} были обновлены",
|
||||
"UserStartedPlayingItemWithValues": "{0} - воспр. «{1}» на {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} - воспр. «{1}» ост-но на {2}",
|
||||
"UserOfflineFromDevice": "{0} отключился с {1}",
|
||||
"UserOnlineFromDevice": "{0} подключился с {1}",
|
||||
"UserPasswordChangedWithName": "Пароль пользователя {0} был изменён",
|
||||
"UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены",
|
||||
"UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} (добавлено в медиатеку)",
|
||||
"ValueSpecialEpisodeName": "Спецэпизод - {0}",
|
||||
"VersionNumber": "Версия {0}"
|
||||
"ValueSpecialEpisodeName": "Специальный эпизод - {0}",
|
||||
"VersionNumber": "Версия {0}",
|
||||
"TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров",
|
||||
"TaskRefreshChannels": "Обновление каналов",
|
||||
"TaskCleanTranscode": "Очистка каталога перекодировки",
|
||||
"TaskUpdatePlugins": "Обновление плагинов",
|
||||
"TaskRefreshPeople": "Обновление метаданных людей",
|
||||
"TaskCleanLogs": "Очистка каталога журналов",
|
||||
"TaskRefreshLibrary": "Сканирование медиатеки",
|
||||
"TaskRefreshChapterImages": "Извлечение изображений сцен",
|
||||
"TaskCleanCache": "Очистка каталога кеша",
|
||||
"TasksChannelsCategory": "Интернет-каналы",
|
||||
"TasksApplicationCategory": "Приложение",
|
||||
"TasksLibraryCategory": "Медиатека",
|
||||
"TasksMaintenanceCategory": "Обслуживание",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Выполняется поиск в Интернете отсутствующих субтитров на основе конфигурации метаданных.",
|
||||
"TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.",
|
||||
"TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.",
|
||||
"TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.",
|
||||
"TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.",
|
||||
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
|
||||
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
|
||||
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
|
||||
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"HeaderContinueWatching": "繼續觀賞",
|
||||
"HeaderFavoriteAlbums": "最愛專輯",
|
||||
"HeaderFavoriteArtists": "最愛演出者",
|
||||
"HeaderFavoriteEpisodes": "最愛級數",
|
||||
"HeaderFavoriteEpisodes": "最愛影集",
|
||||
"HeaderFavoriteShows": "最愛節目",
|
||||
"HeaderFavoriteSongs": "最愛歌曲",
|
||||
"HeaderLiveTV": "電視直播",
|
||||
|
||||
@@ -26,7 +26,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Emby.Server.Implementations.Updates
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages all install, uninstall and update operations (both plugins and system).
|
||||
/// Manages all install, uninstall, and update operations for the system and individual plugins.
|
||||
/// </summary>
|
||||
public class InstallationManager : IInstallationManager
|
||||
{
|
||||
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl";
|
||||
|
||||
/// <summary>
|
||||
/// The _logger.
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
@@ -112,10 +112,10 @@ namespace Emby.Server.Implementations.Updates
|
||||
public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated;
|
||||
public event EventHandler<GenericEventArgs<(IPlugin, VersionInfo)>> PluginUpdated;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
|
||||
public event EventHandler<GenericEventArgs<VersionInfo>> PluginInstalled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
||||
@@ -183,61 +183,56 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PackageVersionInfo> GetCompatibleVersions(
|
||||
IEnumerable<PackageVersionInfo> availableVersions,
|
||||
Version minVersion = null,
|
||||
PackageVersionClass classification = PackageVersionClass.Release)
|
||||
public IEnumerable<VersionInfo> GetCompatibleVersions(
|
||||
IEnumerable<VersionInfo> availableVersions,
|
||||
Version minVersion = null)
|
||||
{
|
||||
var appVer = _applicationHost.ApplicationVersion;
|
||||
availableVersions = availableVersions
|
||||
.Where(x => x.classification == classification
|
||||
&& Version.Parse(x.requiredVersionStr) <= appVer);
|
||||
.Where(x => Version.Parse(x.targetAbi) <= appVer);
|
||||
|
||||
if (minVersion != null)
|
||||
{
|
||||
availableVersions = availableVersions.Where(x => x.Version >= minVersion);
|
||||
availableVersions = availableVersions.Where(x => x.version >= minVersion);
|
||||
}
|
||||
|
||||
return availableVersions.OrderByDescending(x => x.Version);
|
||||
return availableVersions.OrderByDescending(x => x.version);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PackageVersionInfo> GetCompatibleVersions(
|
||||
public IEnumerable<VersionInfo> GetCompatibleVersions(
|
||||
IEnumerable<PackageInfo> availablePackages,
|
||||
string name = null,
|
||||
Guid guid = default,
|
||||
Version minVersion = null,
|
||||
PackageVersionClass classification = PackageVersionClass.Release)
|
||||
Version minVersion = null)
|
||||
{
|
||||
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
|
||||
|
||||
// Package not found.
|
||||
// Package not found in repository
|
||||
if (package == null)
|
||||
{
|
||||
return Enumerable.Empty<PackageVersionInfo>();
|
||||
return Enumerable.Empty<VersionInfo>();
|
||||
}
|
||||
|
||||
return GetCompatibleVersions(
|
||||
package.versions,
|
||||
minVersion,
|
||||
classification);
|
||||
minVersion);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<VersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false);
|
||||
return GetAvailablePluginUpdates(catalog);
|
||||
}
|
||||
|
||||
private IEnumerable<PackageVersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
|
||||
private IEnumerable<VersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
|
||||
{
|
||||
foreach (var plugin in _applicationHost.Plugins)
|
||||
{
|
||||
var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version, _applicationHost.SystemUpdateLevel);
|
||||
var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version);
|
||||
if (version != null
|
||||
&& !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase)))
|
||||
var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
|
||||
var version = compatibleversions.FirstOrDefault(y => y.version > plugin.Version);
|
||||
if (version != null && !CompletedInstallations.Any(x => string.Equals(x.Guid, version.guid, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
yield return version;
|
||||
}
|
||||
@@ -245,7 +240,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken)
|
||||
public async Task InstallPackage(VersionInfo package, CancellationToken cancellationToken)
|
||||
{
|
||||
if (package == null)
|
||||
{
|
||||
@@ -254,11 +249,9 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
var installationInfo = new InstallationInfo
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Guid = package.guid,
|
||||
Name = package.name,
|
||||
AssemblyGuid = package.guid,
|
||||
UpdateClass = package.classification,
|
||||
Version = package.versionStr
|
||||
Version = package.version.ToString()
|
||||
};
|
||||
|
||||
var innerCancellationTokenSource = new CancellationTokenSource();
|
||||
@@ -276,7 +269,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
var installationEventArgs = new InstallationEventArgs
|
||||
{
|
||||
InstallationInfo = installationInfo,
|
||||
PackageVersionInfo = package
|
||||
VersionInfo = package
|
||||
};
|
||||
|
||||
PackageInstalling?.Invoke(this, installationEventArgs);
|
||||
@@ -301,7 +294,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
_currentInstallations.Remove(tuple);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr);
|
||||
_logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.version);
|
||||
|
||||
PackageInstallationCancelled?.Invoke(this, installationEventArgs);
|
||||
|
||||
@@ -337,7 +330,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
/// <param name="package">The package.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns><see cref="Task" />.</returns>
|
||||
private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken)
|
||||
private async Task InstallPackageInternal(VersionInfo package, CancellationToken cancellationToken)
|
||||
{
|
||||
// Set last update time if we were installed before
|
||||
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -349,26 +342,26 @@ namespace Emby.Server.Implementations.Updates
|
||||
// Do plugin-specific processing
|
||||
if (plugin == null)
|
||||
{
|
||||
_logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
|
||||
_logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.version);
|
||||
|
||||
PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo>(package));
|
||||
PluginInstalled?.Invoke(this, new GenericEventArgs<VersionInfo>(package));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
|
||||
_logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.version);
|
||||
|
||||
PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package)));
|
||||
PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package)));
|
||||
}
|
||||
|
||||
_applicationHost.NotifyPendingRestart();
|
||||
}
|
||||
|
||||
private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken)
|
||||
private async Task PerformPackageInstallation(VersionInfo package, CancellationToken cancellationToken)
|
||||
{
|
||||
var extension = Path.GetExtension(package.targetFilename);
|
||||
var extension = Path.GetExtension(package.filename);
|
||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename);
|
||||
_logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.filename);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -415,7 +408,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls a plugin
|
||||
/// Uninstalls a plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin.</param>
|
||||
public void UninstallPlugin(IPlugin plugin)
|
||||
@@ -473,7 +466,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
{
|
||||
lock (_currentInstallationsLock)
|
||||
{
|
||||
var install = _currentInstallations.Find(x => x.info.Id == id);
|
||||
var install = _currentInstallations.Find(x => x.info.Guid == id.ToString());
|
||||
if (install == default((InstallationInfo, CancellationTokenSource)))
|
||||
{
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user