mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-03 06:18:28 +01:00
Merge branch 'master' into PluginDowngrade
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -275,13 +276,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||
|
||||
CertificateInfo = new CertificateInfo
|
||||
{
|
||||
Path = ServerConfigurationManager.Configuration.CertificatePath,
|
||||
Password = ServerConfigurationManager.Configuration.CertificatePassword
|
||||
};
|
||||
Certificate = GetCertificate(CertificateInfo);
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||
@@ -496,6 +490,7 @@ namespace Emby.Server.Implementations
|
||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||
|
||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||
|
||||
_mediaEncoder.SetFFmpegPath();
|
||||
|
||||
@@ -545,6 +540,13 @@ namespace Emby.Server.Implementations
|
||||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||
}
|
||||
|
||||
CertificateInfo = new CertificateInfo
|
||||
{
|
||||
Path = networkConfiguration.CertificatePath,
|
||||
Password = networkConfiguration.CertificatePassword
|
||||
};
|
||||
Certificate = GetCertificate(CertificateInfo);
|
||||
|
||||
DiscoverTypes();
|
||||
|
||||
RegisterServices();
|
||||
@@ -754,7 +756,7 @@ namespace Emby.Server.Implementations
|
||||
// Don't use an empty string password
|
||||
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
|
||||
|
||||
var localCert = new X509Certificate2(certificateLocation, password);
|
||||
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
|
||||
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
||||
if (!localCert.HasPrivateKey)
|
||||
{
|
||||
@@ -911,11 +913,11 @@ namespace Emby.Server.Implementations
|
||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var requiresRestart = false;
|
||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||
|
||||
// Don't do anything if these haven't been set yet
|
||||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
{
|
||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
@@ -935,10 +937,7 @@ namespace Emby.Server.Implementations
|
||||
requiresRestart = true;
|
||||
}
|
||||
|
||||
var currentCertPath = CertificateInfo?.Path;
|
||||
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
|
||||
|
||||
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
|
||||
if (ValidateSslCertificate(networkConfiguration))
|
||||
{
|
||||
requiresRestart = true;
|
||||
}
|
||||
@@ -951,6 +950,33 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SSL certificate.
|
||||
/// </summary>
|
||||
/// <param name="networkConfig">The new configuration.</param>
|
||||
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
|
||||
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
|
||||
{
|
||||
var newPath = networkConfig.CertificatePath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
if (File.Exists(newPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Certificate file '{0}' does not exist.",
|
||||
newPath));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||
/// </summary>
|
||||
|
||||
@@ -88,38 +88,12 @@ namespace Emby.Server.Implementations.Configuration
|
||||
var newConfig = (ServerConfiguration)newConfiguration;
|
||||
|
||||
ValidateMetadataPath(newConfig);
|
||||
ValidateSslCertificate(newConfig);
|
||||
|
||||
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
||||
|
||||
base.ReplaceConfiguration(newConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SSL certificate.
|
||||
/// </summary>
|
||||
/// <param name="newConfig">The new configuration.</param>
|
||||
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
|
||||
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
|
||||
{
|
||||
var serverConfig = (ServerConfiguration)newConfig;
|
||||
|
||||
var newPath = serverConfig.CertificatePath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
if (!File.Exists(newPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Certificate file '{0}' does not exist.",
|
||||
newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the metadata path.
|
||||
/// </summary>
|
||||
|
||||
@@ -1138,7 +1138,10 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (episodeSeries != null)
|
||||
{
|
||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
{
|
||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,7 +1188,10 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (series != null)
|
||||
{
|
||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
||||
AttachPrimaryImageAspectRatio(dto, series);
|
||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
{
|
||||
AttachPrimaryImageAspectRatio(dto, series);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<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="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<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" />
|
||||
|
||||
@@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
authInfo.IsApiKey = true;
|
||||
authInfo.IsApiKey = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
authInfo.IsApiKey = false;
|
||||
authInfo.IsApiKey = true;
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// A library post scan/refresh task for pre-fetching remote images.
|
||||
/// </summary>
|
||||
public class ImageFetcherPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ILogger<ImageFetcherPostScanTask> _logger;
|
||||
private readonly SemaphoreSlim _imageFetcherLock;
|
||||
|
||||
private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
|
||||
/// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
|
||||
public ImageFetcherPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
IProviderManager providerManager,
|
||||
ILogger<ImageFetcherPostScanTask> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_providerManager = providerManager;
|
||||
_logger = logger;
|
||||
_queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
|
||||
_imageFetcherLock = new SemaphoreSlim(1, 1);
|
||||
_libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
|
||||
_libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
|
||||
_providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
// Sometimes a library scan will cause this to run twice if there's an item refresh going on.
|
||||
await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var itemGuids = _queuedItems.Keys.ToList();
|
||||
|
||||
for (var i = 0; i < itemGuids.Count; i++)
|
||||
{
|
||||
if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
var itemType = queuedItem.item.GetType();
|
||||
_logger.LogDebug(
|
||||
"Updating remote images for item {ItemId} with media type {ItemMediaType}",
|
||||
itemId,
|
||||
itemType);
|
||||
try
|
||||
{
|
||||
await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
|
||||
}
|
||||
|
||||
_queuedItems.TryRemove(queuedItem.item.Id, out _);
|
||||
}
|
||||
|
||||
if (itemGuids.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
|
||||
itemGuids.Count.ToString(CultureInfo.InvariantCulture),
|
||||
(DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No images were updated.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_imageFetcherLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
|
||||
{
|
||||
if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
|
||||
{
|
||||
_queuedItems.AddOrUpdate(
|
||||
itemChangeEventArgs.Item.Id,
|
||||
(itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
|
||||
(key, existingValue) => existingValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
|
||||
{
|
||||
_queuedItems.AddOrUpdate(
|
||||
e.Argument.Id,
|
||||
(e.Argument, ItemUpdateType.None),
|
||||
(key, existingValue) => existingValue);
|
||||
}
|
||||
|
||||
// The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
|
||||
// the item that was refreshed regardless of children refreshes. So we take it as a signal
|
||||
// that the refresh is entirely completed.
|
||||
Run(null, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,6 @@ using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Providers.MediaInfo;
|
||||
@@ -1955,9 +1954,12 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
RunMetadataSavers(items, updateReason);
|
||||
foreach (var item in items)
|
||||
{
|
||||
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
@@ -1988,25 +1990,22 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
|
||||
|
||||
public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason)
|
||||
public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||
{
|
||||
foreach (var item in items)
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
ProviderManager.SaveMetadata(item, updateReason);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
ProviderManager.SaveMetadata(item, updateReason);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
|
||||
{
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||
|
||||
protected override Book Resolve(ItemResolveArgs args)
|
||||
{
|
||||
|
||||
@@ -139,13 +139,13 @@ namespace Emby.Server.Implementations.Library
|
||||
return list
|
||||
.OrderBy(i =>
|
||||
{
|
||||
var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
var index = orders.IndexOf(i.Id.ToString("D", CultureInfo.InvariantCulture));
|
||||
|
||||
if (index == -1
|
||||
&& i is UserView view
|
||||
&& view.DisplayParentId != Guid.Empty)
|
||||
{
|
||||
index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture));
|
||||
index = orders.IndexOf(view.DisplayParentId.ToString("D", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return index == -1 ? int.MaxValue : index;
|
||||
|
||||
@@ -611,25 +611,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
CancellationToken cancellationToken,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
try
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_tokens.Clear();
|
||||
|
||||
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
|
||||
{
|
||||
enableRetry = false;
|
||||
}
|
||||
|
||||
if (!enableRetry)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Response is automatically disposed in the calling function,
|
||||
// so dispose manually if not returning.
|
||||
response.Dispose();
|
||||
if (!enableRetry || (int)response.StatusCode >= 500)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
_tokens.Clear();
|
||||
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
|
||||
return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -647,6 +647,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
|
||||
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
||||
@@ -701,6 +702,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
try
|
||||
{
|
||||
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var response = httpResponse.Content;
|
||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
|
||||
@@ -709,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// Apparently we're supposed to swallow this
|
||||
// SchedulesDirect returns 400 if no lineups are configured.
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
foreach (var programDto in currentProgramDtos)
|
||||
{
|
||||
if (currentChannelsDict.TryGetValue(programDto.ChannelId, out BaseItemDto channelDto))
|
||||
if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto))
|
||||
{
|
||||
channelDto.CurrentProgram = programDto;
|
||||
}
|
||||
@@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
|
||||
|
||||
info.Name = program.Name;
|
||||
info.ChannelId = programDto.ChannelId;
|
||||
info.ChannelId = programDto.ChannelId ?? Guid.Empty;
|
||||
info.ChannelName = programDto.ChannelName;
|
||||
info.StartDate = program.StartDate;
|
||||
info.Name = program.Name;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
internal class Channels
|
||||
{
|
||||
public string GuideNumber { get; set; }
|
||||
|
||||
public string GuideName { get; set; }
|
||||
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public string URL { get; set; }
|
||||
|
||||
public bool Favorite { get; set; }
|
||||
|
||||
public bool DRM { get; set; }
|
||||
|
||||
public bool HD { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
internal class DiscoverResponse
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
public string ModelNumber { get; set; }
|
||||
|
||||
public string FirmwareName { get; set; }
|
||||
|
||||
public string FirmwareVersion { get; set; }
|
||||
|
||||
public string DeviceID { get; set; }
|
||||
|
||||
public string DeviceAuth { get; set; }
|
||||
|
||||
public string BaseURL { get; set; }
|
||||
|
||||
public string LineupURL { get; set; }
|
||||
|
||||
public int TunerCount { get; set; }
|
||||
|
||||
public bool SupportsTranscoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var model = ModelNumber ?? string.Empty;
|
||||
|
||||
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||
|
||||
public HdHomerunHost(
|
||||
@@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
_socketFactory = socketFactory;
|
||||
_networkManager = networkManager;
|
||||
_streamHelper = streamHelper;
|
||||
|
||||
_jsonOptions = JsonDefaults.GetOptions();
|
||||
}
|
||||
|
||||
public string Name => "HD Homerun";
|
||||
@@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||
=> ChannelIdPrefix + i.GuideNumber;
|
||||
|
||||
private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
|
||||
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false) ?? new List<Channels>();
|
||||
|
||||
if (info.ImportFavoritesOnly)
|
||||
@@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
Id = GetChannelId(info, i),
|
||||
IsFavorite = i.Favorite,
|
||||
TunerHostId = info.Id,
|
||||
IsHD = i.HD == 1,
|
||||
IsHD = i.HD,
|
||||
AudioCodec = i.AudioCodec,
|
||||
VideoCodec = i.VideoCodec,
|
||||
ChannelType = ChannelType.TV,
|
||||
@@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}).Cast<ChannelInfo>().ToList();
|
||||
}
|
||||
|
||||
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
|
||||
internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = info.Id;
|
||||
|
||||
@@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
try
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
@@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return new Uri(url).AbsoluteUri.TrimEnd('/');
|
||||
}
|
||||
|
||||
private class Channels
|
||||
{
|
||||
public string GuideNumber { get; set; }
|
||||
|
||||
public string GuideName { get; set; }
|
||||
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public string URL { get; set; }
|
||||
|
||||
public bool Favorite { get; set; }
|
||||
|
||||
public bool DRM { get; set; }
|
||||
|
||||
public int HD { get; set; }
|
||||
}
|
||||
|
||||
protected EncodingOptions GetEncodingOptions()
|
||||
{
|
||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
||||
@@ -674,42 +662,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
public class DiscoverResponse
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
public string ModelNumber { get; set; }
|
||||
|
||||
public string FirmwareName { get; set; }
|
||||
|
||||
public string FirmwareVersion { get; set; }
|
||||
|
||||
public string DeviceID { get; set; }
|
||||
|
||||
public string DeviceAuth { get; set; }
|
||||
|
||||
public string BaseURL { get; set; }
|
||||
|
||||
public string LineupURL { get; set; }
|
||||
|
||||
public int TunerCount { get; set; }
|
||||
|
||||
public bool SupportsTranscoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var model = ModelNumber ?? string.Empty;
|
||||
|
||||
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_modelCache)
|
||||
@@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return list;
|
||||
}
|
||||
|
||||
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||
internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var hostInfo = new TunerHostInfo
|
||||
{
|
||||
@@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
hostInfo.DeviceId = modelInfo.DeviceID;
|
||||
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
||||
hostInfo.TunerCount = modelInfo.TunerCount;
|
||||
|
||||
return hostInfo;
|
||||
}
|
||||
|
||||
@@ -113,5 +113,7 @@
|
||||
"TasksChannelsCategory": "کانالهای داخلی",
|
||||
"TasksApplicationCategory": "برنامه",
|
||||
"TasksLibraryCategory": "کتابخانه",
|
||||
"TasksMaintenanceCategory": "تعمیر"
|
||||
"TasksMaintenanceCategory": "تعمیر",
|
||||
"Forced": "اجباری",
|
||||
"Default": "پیشفرض"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "Application : {0}, Appareil : {1}",
|
||||
"AppDeviceValues": "App : {0}, Appareil : {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
|
||||
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
|
||||
"Books": "Livres",
|
||||
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
|
||||
"Channels": "Chaînes",
|
||||
@@ -11,12 +11,12 @@
|
||||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||
"DeviceOnlineWithName": "{0} est connecté",
|
||||
"FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
||||
"Favorites": "Favoris",
|
||||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artistes de l'album",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderContinueWatching": "Reprendre le visionnement",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes favoris",
|
||||
"HeaderFavoriteEpisodes": "Épisodes favoris",
|
||||
@@ -26,12 +26,12 @@
|
||||
"HeaderNextUp": "À Suivre",
|
||||
"HeaderRecordingGroups": "Groupes d'enregistrements",
|
||||
"HomeVideos": "Vidéos personnelles",
|
||||
"Inherit": "Hériter",
|
||||
"Inherit": "Hérite",
|
||||
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
|
||||
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
|
||||
"LabelIpAddressValue": "Adresse IP : {0}",
|
||||
"LabelRunningTimeValue": "Durée : {0}",
|
||||
"Latest": "Derniers",
|
||||
"Latest": "Plus récent",
|
||||
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
|
||||
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
|
||||
@@ -40,15 +40,15 @@
|
||||
"Movies": "Films",
|
||||
"Music": "Musique",
|
||||
"MusicVideos": "Vidéos musicales",
|
||||
"NameInstallFailed": "{0} échec d'installation",
|
||||
"NameInstallFailed": "échec d'installation de {0}",
|
||||
"NameSeasonNumber": "Saison {0}",
|
||||
"NameSeasonUnknown": "Saison Inconnue",
|
||||
"NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.",
|
||||
"NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
|
||||
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
|
||||
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
|
||||
"NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
|
||||
"NotificationOptionCameraImageUploaded": "Image d'appareil photo transférée",
|
||||
"NotificationOptionInstallationFailed": "Échec d'installation",
|
||||
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
|
||||
"NotificationOptionPluginError": "Erreur d'extension",
|
||||
@@ -70,9 +70,9 @@
|
||||
"ScheduledTaskFailedWithName": "{0} a échoué",
|
||||
"ScheduledTaskStartedWithName": "{0} a commencé",
|
||||
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
|
||||
"Shows": "Émissions",
|
||||
"Shows": "Séries",
|
||||
"Songs": "Chansons",
|
||||
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
|
||||
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
||||
"Sync": "Synchroniser",
|
||||
@@ -80,39 +80,43 @@
|
||||
"TvShows": "Séries Télé",
|
||||
"User": "Utilisateur",
|
||||
"UserCreatedWithName": "L'utilisateur {0} a été créé",
|
||||
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
|
||||
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
|
||||
"UserDeletedWithName": "L'utilisateur {0} supprimé",
|
||||
"UserDownloadingItemWithValues": "{0} télécharge {1}",
|
||||
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
|
||||
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
|
||||
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
|
||||
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
|
||||
"UserOfflineFromDevice": "{0} s'est déconnecté de {1}",
|
||||
"UserOnlineFromDevice": "{0} s'est connecté de {1}",
|
||||
"UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié",
|
||||
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
|
||||
"ValueSpecialEpisodeName": "Spécial - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TasksLibraryCategory": "Bibliothèque",
|
||||
"TasksLibraryCategory": "Médiathèque",
|
||||
"TasksMaintenanceCategory": "Entretien",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquant sur l'internet selon la configuration des métadonnées.",
|
||||
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
|
||||
"TaskRefreshChannels": "Rafraîchir des chaines",
|
||||
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines internet.",
|
||||
"TaskRefreshChannels": "Rafraîchir les chaines",
|
||||
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage datant de plus d'un jour.",
|
||||
"TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
|
||||
"TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.",
|
||||
"TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour les m.à.j. automatiques.",
|
||||
"TaskUpdatePlugins": "Mise à jour des extensions",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.",
|
||||
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
|
||||
"TaskRefreshPeople": "Rafraîchir les personnes",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux plus vieux que {0} jours.",
|
||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||
"TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshLibraryDescription": "Analyse votre médiathèque pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres.",
|
||||
"TaskRefreshLibrary": "Analyser la bibliothèque de médias",
|
||||
"TaskRefreshLibrary": "Analyser la médiathèque",
|
||||
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
|
||||
"TasksChannelsCategory": "Canaux Internet",
|
||||
"Default": "Par défaut"
|
||||
"TasksChannelsCategory": "Chaines Internet",
|
||||
"Default": "Par défaut",
|
||||
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
|
||||
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
|
||||
"Undefined": "Indéfini",
|
||||
"Forced": "Forcé"
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
|
||||
"LabelRunningTimeValue": "Garums: {0}",
|
||||
"Inherit": "Mantot",
|
||||
"AppDeviceValues": "Lietotne:{0}, Ierīce:{1}",
|
||||
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots tavai multvides bibliotēkai",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
|
||||
"UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
|
||||
@@ -95,7 +95,7 @@
|
||||
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
|
||||
"TasksApplicationCategory": "Lietotne",
|
||||
"TasksLibraryCategory": "Bibliotēka",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus pēc metadatu uzstādījumiem.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
|
||||
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
|
||||
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
||||
"TaskRefreshChannels": "Atjaunot Kanālus",
|
||||
@@ -103,14 +103,19 @@
|
||||
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
|
||||
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
|
||||
"TaskUpdatePlugins": "Atjaunot Paplašinājumus",
|
||||
"TaskRefreshPeopleDescription": "Atjauno metadatus priekš aktieriem un direktoriem tavā mediju bibliotēkā.",
|
||||
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
|
||||
"TaskRefreshPeople": "Atjaunot Cilvēkus",
|
||||
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
|
||||
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
|
||||
"TaskRefreshLibraryDescription": "Skenē tavas mediju bibliotēkas priekš jaunām datnēm un atjauno metadatus.",
|
||||
"TaskRefreshLibrary": "Skanēt Mediju Bibliotēku",
|
||||
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
|
||||
"TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
|
||||
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
|
||||
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
|
||||
"TasksChannelsCategory": "Interneta Kanāli",
|
||||
"TasksMaintenanceCategory": "Apkope"
|
||||
"TasksMaintenanceCategory": "Apkope",
|
||||
"Forced": "Piespiests",
|
||||
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
|
||||
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
|
||||
"Undefined": "Nenoteikts",
|
||||
"Default": "Noklusējums"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,10 @@
|
||||
"TasksApplicationCategory": "Aplikacija",
|
||||
"TasksLibraryCategory": "Knjižnica",
|
||||
"TasksMaintenanceCategory": "Vzdrževanje",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu."
|
||||
"TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu.",
|
||||
"TaskCleanActivityLogDescription": "Počisti zapise v dnevniku aktivnosti starejše od nastavljenega časa.",
|
||||
"TaskCleanActivityLog": "Počisti dnevnik aktivnosti",
|
||||
"Undefined": "Nedoločen",
|
||||
"Forced": "Prisilno",
|
||||
"Default": "Privzeto"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
|
||||
@@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<SessionEventArgs> SessionActivity;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all connections.
|
||||
/// </summary>
|
||||
@@ -312,6 +315,19 @@ namespace Emby.Server.Implementations.Session
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnSessionControllerConnected(SessionInfo info)
|
||||
{
|
||||
EventHelper.QueueEventIfNotNull(
|
||||
SessionControllerConnected,
|
||||
this,
|
||||
new SessionEventArgs
|
||||
{
|
||||
SessionInfo = info
|
||||
},
|
||||
_logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CloseIfNeeded(SessionInfo session)
|
||||
{
|
||||
|
||||
@@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
var controller = (WebSocketController)controllerInfo.Item1;
|
||||
controller.AddWebSocket(connection);
|
||||
|
||||
_sessionManager.OnSessionControllerConnected(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The map between users and counter of active sessions.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<Guid, int> _activeUsers =
|
||||
new ConcurrentDictionary<Guid, int>();
|
||||
|
||||
/// <summary>
|
||||
/// The map between sessions and groups.
|
||||
/// </summary>
|
||||
@@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
||||
_sessionManager.SessionStarted += OnSessionManagerSessionStarted;
|
||||
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
throw new InvalidOperationException("Could not add session to group!");
|
||||
}
|
||||
|
||||
UpdateSessionsCounter(session.UserId, 1);
|
||||
group.CreateGroup(session, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
if (existingGroup.GroupId.Equals(request.GroupId))
|
||||
{
|
||||
// Restore session.
|
||||
UpdateSessionsCounter(session.UserId, 1);
|
||||
group.SessionJoin(session, request, cancellationToken);
|
||||
return;
|
||||
}
|
||||
@@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
throw new InvalidOperationException("Could not add session to group!");
|
||||
}
|
||||
|
||||
UpdateSessionsCounter(session.UserId, 1);
|
||||
group.SessionJoin(session, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
throw new InvalidOperationException("Could not remove session from group!");
|
||||
}
|
||||
|
||||
UpdateSessionsCounter(session.UserId, -1);
|
||||
group.SessionLeave(session, request, cancellationToken);
|
||||
|
||||
if (group.IsGroupEmpty())
|
||||
@@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsUserActive(Guid userId)
|
||||
{
|
||||
if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
|
||||
{
|
||||
return sessionsCounter > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
@@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
return;
|
||||
}
|
||||
|
||||
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
||||
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
|
||||
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
@@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
JoinGroup(session, request, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSessionsCounter(Guid userId, int toAdd)
|
||||
{
|
||||
// Update sessions counter.
|
||||
var newSessionsCounter = _activeUsers.AddOrUpdate(
|
||||
userId,
|
||||
1,
|
||||
(key, sessionsCounter) => sessionsCounter + toAdd);
|
||||
|
||||
// Should never happen.
|
||||
if (newSessionsCounter < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Sessions counter is negative!");
|
||||
}
|
||||
|
||||
// Clean record if user has no more active sessions.
|
||||
if (newSessionsCounter == 0)
|
||||
{
|
||||
_activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
|
||||
// Don't add a package that doesn't have any compatible versions.
|
||||
if (package.Versions.Count == 0)
|
||||
if (package.versions.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -555,6 +555,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
||||
|
||||
Reference in New Issue
Block a user