mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-31 14:33:54 -04:00
Merge branch 'master' into PluginDowngrade
This commit is contained in:
commit
67c480ad53
@ -340,10 +340,19 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playlist = new PlaylistItem[len];
|
var playlist = new PlaylistItem[len];
|
||||||
playlist[0] = CreatePlaylistItem(items[0], user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex);
|
|
||||||
|
// Not nullable enabled - so this is required.
|
||||||
|
playlist[0] = CreatePlaylistItem(
|
||||||
|
items[0],
|
||||||
|
user,
|
||||||
|
command.StartPositionTicks ?? 0,
|
||||||
|
command.MediaSourceId ?? string.Empty,
|
||||||
|
command.AudioStreamIndex,
|
||||||
|
command.SubtitleStreamIndex);
|
||||||
|
|
||||||
for (int i = 1; i < len; i++)
|
for (int i = 1; i < len; i++)
|
||||||
{
|
{
|
||||||
playlist[i] = CreatePlaylistItem(items[i], user, 0, null, null, null);
|
playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -275,13 +276,6 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
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;
|
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||||
@ -496,6 +490,7 @@ namespace Emby.Server.Implementations
|
|||||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||||
|
|
||||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
|
||||||
_mediaEncoder.SetFFmpegPath();
|
_mediaEncoder.SetFFmpegPath();
|
||||||
|
|
||||||
@ -545,6 +540,13 @@ namespace Emby.Server.Implementations
|
|||||||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CertificateInfo = new CertificateInfo
|
||||||
|
{
|
||||||
|
Path = networkConfiguration.CertificatePath,
|
||||||
|
Password = networkConfiguration.CertificatePassword
|
||||||
|
};
|
||||||
|
Certificate = GetCertificate(CertificateInfo);
|
||||||
|
|
||||||
DiscoverTypes();
|
DiscoverTypes();
|
||||||
|
|
||||||
RegisterServices();
|
RegisterServices();
|
||||||
@ -754,7 +756,7 @@ namespace Emby.Server.Implementations
|
|||||||
// Don't use an empty string password
|
// Don't use an empty string password
|
||||||
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.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;
|
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
||||||
if (!localCert.HasPrivateKey)
|
if (!localCert.HasPrivateKey)
|
||||||
{
|
{
|
||||||
@ -911,11 +913,11 @@ namespace Emby.Server.Implementations
|
|||||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var requiresRestart = false;
|
var requiresRestart = false;
|
||||||
|
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|
||||||
// Don't do anything if these haven't been set yet
|
// Don't do anything if these haven't been set yet
|
||||||
if (HttpPort != 0 && HttpsPort != 0)
|
if (HttpPort != 0 && HttpsPort != 0)
|
||||||
{
|
{
|
||||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
|
||||||
// Need to restart if ports have changed
|
// Need to restart if ports have changed
|
||||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||||
@ -935,10 +937,7 @@ namespace Emby.Server.Implementations
|
|||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentCertPath = CertificateInfo?.Path;
|
if (ValidateSslCertificate(networkConfiguration))
|
||||||
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
|
|
||||||
|
|
||||||
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
requiresRestart = true;
|
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>
|
/// <summary>
|
||||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -88,38 +88,12 @@ namespace Emby.Server.Implementations.Configuration
|
|||||||
var newConfig = (ServerConfiguration)newConfiguration;
|
var newConfig = (ServerConfiguration)newConfiguration;
|
||||||
|
|
||||||
ValidateMetadataPath(newConfig);
|
ValidateMetadataPath(newConfig);
|
||||||
ValidateSslCertificate(newConfig);
|
|
||||||
|
|
||||||
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
||||||
|
|
||||||
base.ReplaceConfiguration(newConfiguration);
|
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>
|
/// <summary>
|
||||||
/// Validates the metadata path.
|
/// Validates the metadata path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1138,7 +1138,10 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (episodeSeries != null)
|
if (episodeSeries != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
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)
|
if (series != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
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.ResponseCompression" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" 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.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.Caching.Memory" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||||
|
@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
authInfo.IsApiKey = true;
|
authInfo.IsApiKey = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
authInfo.IsApiKey = false;
|
authInfo.IsApiKey = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateToken)
|
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.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Library;
|
using MediaBrowser.Model.Library;
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using MediaBrowser.Providers.MediaInfo;
|
using MediaBrowser.Providers.MediaInfo;
|
||||||
@ -1955,9 +1954,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
_itemRepository.SaveItems(items, cancellationToken);
|
||||||
|
|
||||||
@ -1988,25 +1990,22 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> UpdateItemsAsync(new[] { item }, parent, updateReason, 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);
|
||||||
{
|
|
||||||
ProviderManager.SaveMetadata(item, updateReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.DateLastSaved = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.DateLastSaved = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
{
|
{
|
||||||
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
|
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)
|
protected override Book Resolve(ItemResolveArgs args)
|
||||||
{
|
{
|
||||||
|
@ -139,13 +139,13 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return list
|
return list
|
||||||
.OrderBy(i =>
|
.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
|
if (index == -1
|
||||||
&& i is UserView view
|
&& i is UserView view
|
||||||
&& view.DisplayParentId != Guid.Empty)
|
&& 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;
|
return index == -1 ? int.MaxValue : index;
|
||||||
|
@ -611,25 +611,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
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);
|
return response;
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
_tokens.Clear();
|
|
||||||
|
|
||||||
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
|
|
||||||
{
|
|
||||||
enableRetry = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableRetry)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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));
|
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
|
||||||
return await Send(options, false, 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);
|
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||||
|
|
||||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
|
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
|
||||||
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
||||||
@ -701,6 +702,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var response = httpResponse.Content;
|
using var response = httpResponse.Content;
|
||||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
|
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
|
||||||
@ -709,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
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)
|
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||||||
|
|
||||||
foreach (var programDto in currentProgramDtos)
|
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;
|
channelDto.CurrentProgram = programDto;
|
||||||
}
|
}
|
||||||
@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||||||
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
|
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
|
||||||
|
|
||||||
info.Name = program.Name;
|
info.Name = program.Name;
|
||||||
info.ChannelId = programDto.ChannelId;
|
info.ChannelId = programDto.ChannelId ?? Guid.Empty;
|
||||||
info.ChannelName = programDto.ChannelName;
|
info.ChannelName = programDto.ChannelName;
|
||||||
info.StartDate = program.StartDate;
|
info.StartDate = program.StartDate;
|
||||||
info.Name = program.Name;
|
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;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly IStreamHelper _streamHelper;
|
private readonly IStreamHelper _streamHelper;
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
|
||||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||||
|
|
||||||
public HdHomerunHost(
|
public HdHomerunHost(
|
||||||
@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
_socketFactory = socketFactory;
|
_socketFactory = socketFactory;
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
_streamHelper = streamHelper;
|
_streamHelper = streamHelper;
|
||||||
|
|
||||||
|
_jsonOptions = JsonDefaults.GetOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "HD Homerun";
|
public string Name => "HD Homerun";
|
||||||
@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
private string GetChannelId(TunerHostInfo info, Channels i)
|
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||||
=> ChannelIdPrefix + i.GuideNumber;
|
=> 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);
|
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);
|
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);
|
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>();
|
.ConfigureAwait(false) ?? new List<Channels>();
|
||||||
|
|
||||||
if (info.ImportFavoritesOnly)
|
if (info.ImportFavoritesOnly)
|
||||||
@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
Id = GetChannelId(info, i),
|
Id = GetChannelId(info, i),
|
||||||
IsFavorite = i.Favorite,
|
IsFavorite = i.Favorite,
|
||||||
TunerHostId = info.Id,
|
TunerHostId = info.Id,
|
||||||
IsHD = i.HD == 1,
|
IsHD = i.HD,
|
||||||
AudioCodec = i.AudioCodec,
|
AudioCodec = i.AudioCodec,
|
||||||
VideoCodec = i.VideoCodec,
|
VideoCodec = i.VideoCodec,
|
||||||
ChannelType = ChannelType.TV,
|
ChannelType = ChannelType.TV,
|
||||||
@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
}).Cast<ChannelInfo>().ToList();
|
}).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;
|
var cacheKey = info.Id;
|
||||||
|
|
||||||
@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
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);
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
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);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cacheKey))
|
if (!string.IsNullOrEmpty(cacheKey))
|
||||||
@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
return new Uri(url).AbsoluteUri.TrimEnd('/');
|
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()
|
protected EncodingOptions GetEncodingOptions()
|
||||||
{
|
{
|
||||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
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)
|
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
lock (_modelCache)
|
lock (_modelCache)
|
||||||
@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var hostInfo = new TunerHostInfo
|
var hostInfo = new TunerHostInfo
|
||||||
{
|
{
|
||||||
@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
|
|
||||||
hostInfo.DeviceId = modelInfo.DeviceID;
|
hostInfo.DeviceId = modelInfo.DeviceID;
|
||||||
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
||||||
|
hostInfo.TunerCount = modelInfo.TunerCount;
|
||||||
|
|
||||||
return hostInfo;
|
return hostInfo;
|
||||||
}
|
}
|
||||||
|
@ -113,5 +113,7 @@
|
|||||||
"TasksChannelsCategory": "کانالهای داخلی",
|
"TasksChannelsCategory": "کانالهای داخلی",
|
||||||
"TasksApplicationCategory": "برنامه",
|
"TasksApplicationCategory": "برنامه",
|
||||||
"TasksLibraryCategory": "کتابخانه",
|
"TasksLibraryCategory": "کتابخانه",
|
||||||
"TasksMaintenanceCategory": "تعمیر"
|
"TasksMaintenanceCategory": "تعمیر",
|
||||||
|
"Forced": "اجباری",
|
||||||
|
"Default": "پیشفرض"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"Albums": "Albums",
|
"Albums": "Albums",
|
||||||
"AppDeviceValues": "Application : {0}, Appareil : {1}",
|
"AppDeviceValues": "App : {0}, Appareil : {1}",
|
||||||
"Application": "Application",
|
"Application": "Application",
|
||||||
"Artists": "Artistes",
|
"Artists": "Artistes",
|
||||||
"AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
|
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
|
||||||
"Books": "Livres",
|
"Books": "Livres",
|
||||||
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
|
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
|
||||||
"Channels": "Chaînes",
|
"Channels": "Chaînes",
|
||||||
@ -11,12 +11,12 @@
|
|||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||||
"DeviceOnlineWithName": "{0} est connecté",
|
"DeviceOnlineWithName": "{0} est connecté",
|
||||||
"FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
|
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
||||||
"Favorites": "Favoris",
|
"Favorites": "Favoris",
|
||||||
"Folders": "Dossiers",
|
"Folders": "Dossiers",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
"HeaderAlbumArtists": "Artistes de l'album",
|
"HeaderAlbumArtists": "Artistes de l'album",
|
||||||
"HeaderContinueWatching": "Continuer à regarder",
|
"HeaderContinueWatching": "Reprendre le visionnement",
|
||||||
"HeaderFavoriteAlbums": "Albums favoris",
|
"HeaderFavoriteAlbums": "Albums favoris",
|
||||||
"HeaderFavoriteArtists": "Artistes favoris",
|
"HeaderFavoriteArtists": "Artistes favoris",
|
||||||
"HeaderFavoriteEpisodes": "Épisodes favoris",
|
"HeaderFavoriteEpisodes": "Épisodes favoris",
|
||||||
@ -26,12 +26,12 @@
|
|||||||
"HeaderNextUp": "À Suivre",
|
"HeaderNextUp": "À Suivre",
|
||||||
"HeaderRecordingGroups": "Groupes d'enregistrements",
|
"HeaderRecordingGroups": "Groupes d'enregistrements",
|
||||||
"HomeVideos": "Vidéos personnelles",
|
"HomeVideos": "Vidéos personnelles",
|
||||||
"Inherit": "Hériter",
|
"Inherit": "Hérite",
|
||||||
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
|
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
|
||||||
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
|
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
|
||||||
"LabelIpAddressValue": "Adresse IP : {0}",
|
"LabelIpAddressValue": "Adresse IP : {0}",
|
||||||
"LabelRunningTimeValue": "Durée : {0}",
|
"LabelRunningTimeValue": "Durée : {0}",
|
||||||
"Latest": "Derniers",
|
"Latest": "Plus récent",
|
||||||
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
|
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
|
||||||
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
|
"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",
|
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
|
||||||
@ -40,15 +40,15 @@
|
|||||||
"Movies": "Films",
|
"Movies": "Films",
|
||||||
"Music": "Musique",
|
"Music": "Musique",
|
||||||
"MusicVideos": "Vidéos musicales",
|
"MusicVideos": "Vidéos musicales",
|
||||||
"NameInstallFailed": "{0} échec d'installation",
|
"NameInstallFailed": "échec d'installation de {0}",
|
||||||
"NameSeasonNumber": "Saison {0}",
|
"NameSeasonNumber": "Saison {0}",
|
||||||
"NameSeasonUnknown": "Saison Inconnue",
|
"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",
|
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
|
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
|
||||||
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
|
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêté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",
|
"NotificationOptionInstallationFailed": "Échec d'installation",
|
||||||
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
|
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
|
||||||
"NotificationOptionPluginError": "Erreur d'extension",
|
"NotificationOptionPluginError": "Erreur d'extension",
|
||||||
@ -70,9 +70,9 @@
|
|||||||
"ScheduledTaskFailedWithName": "{0} a échoué",
|
"ScheduledTaskFailedWithName": "{0} a échoué",
|
||||||
"ScheduledTaskStartedWithName": "{0} a commencé",
|
"ScheduledTaskStartedWithName": "{0} a commencé",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
|
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
|
||||||
"Shows": "Émissions",
|
"Shows": "Séries",
|
||||||
"Songs": "Chansons",
|
"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}",
|
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||||
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
||||||
"Sync": "Synchroniser",
|
"Sync": "Synchroniser",
|
||||||
@ -80,39 +80,43 @@
|
|||||||
"TvShows": "Séries Télé",
|
"TvShows": "Séries Télé",
|
||||||
"User": "Utilisateur",
|
"User": "Utilisateur",
|
||||||
"UserCreatedWithName": "L'utilisateur {0} a été créé",
|
"UserCreatedWithName": "L'utilisateur {0} a été créé",
|
||||||
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
|
"UserDeletedWithName": "L'utilisateur {0} supprimé",
|
||||||
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
|
"UserDownloadingItemWithValues": "{0} télécharge {1}",
|
||||||
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
|
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
|
||||||
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
|
"UserOfflineFromDevice": "{0} s'est déconnecté de {1}",
|
||||||
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
|
"UserOnlineFromDevice": "{0} s'est connecté de {1}",
|
||||||
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
|
"UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié",
|
||||||
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
|
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
|
"UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
|
"UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
|
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
|
||||||
"ValueSpecialEpisodeName": "Spécial - {0}",
|
"ValueSpecialEpisodeName": "Spécial - {0}",
|
||||||
"VersionNumber": "Version {0}",
|
"VersionNumber": "Version {0}",
|
||||||
"TasksLibraryCategory": "Bibliothèque",
|
"TasksLibraryCategory": "Médiathèque",
|
||||||
"TasksMaintenanceCategory": "Entretien",
|
"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",
|
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
|
||||||
"TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
|
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines internet.",
|
||||||
"TaskRefreshChannels": "Rafraîchir des chaines",
|
"TaskRefreshChannels": "Rafraîchir les chaines",
|
||||||
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
|
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage datant de plus d'un jour.",
|
||||||
"TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
|
"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",
|
"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.",
|
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
|
||||||
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
"TaskRefreshPeople": "Rafraîchir les personnes",
|
||||||
"TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
|
"TaskCleanLogsDescription": "Supprime les journaux plus vieux que {0} jours.",
|
||||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
"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",
|
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||||
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres.",
|
"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",
|
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
|
||||||
"TasksApplicationCategory": "Application",
|
"TasksApplicationCategory": "Application",
|
||||||
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
|
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
|
||||||
"TasksChannelsCategory": "Canaux Internet",
|
"TasksChannelsCategory": "Chaines Internet",
|
||||||
"Default": "Par défaut"
|
"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",
|
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
|
||||||
"LabelRunningTimeValue": "Garums: {0}",
|
"LabelRunningTimeValue": "Garums: {0}",
|
||||||
"Inherit": "Mantot",
|
"Inherit": "Mantot",
|
||||||
"AppDeviceValues": "Lietotne:{0}, Ierīce:{1}",
|
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
|
||||||
"VersionNumber": "Versija {0}",
|
"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}",
|
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
|
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
|
||||||
"UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
|
"UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
|
||||||
@ -95,7 +95,7 @@
|
|||||||
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
|
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
|
||||||
"TasksApplicationCategory": "Lietotne",
|
"TasksApplicationCategory": "Lietotne",
|
||||||
"TasksLibraryCategory": "Bibliotēka",
|
"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",
|
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
|
||||||
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
||||||
"TaskRefreshChannels": "Atjaunot Kanālus",
|
"TaskRefreshChannels": "Atjaunot Kanālus",
|
||||||
@ -103,14 +103,19 @@
|
|||||||
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
|
"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.",
|
"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",
|
"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",
|
"TaskRefreshPeople": "Atjaunot Cilvēkus",
|
||||||
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
|
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
|
||||||
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
|
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
|
||||||
"TaskRefreshLibraryDescription": "Skenē tavas mediju bibliotēkas priekš jaunām datnēm un atjauno metadatus.",
|
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
|
||||||
"TaskRefreshLibrary": "Skanēt Mediju Bibliotēku",
|
"TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
|
||||||
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
|
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
|
||||||
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
|
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
|
||||||
"TasksChannelsCategory": "Interneta Kanāli",
|
"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",
|
"TasksApplicationCategory": "Aplikacija",
|
||||||
"TasksLibraryCategory": "Knjižnica",
|
"TasksLibraryCategory": "Knjižnica",
|
||||||
"TasksMaintenanceCategory": "Vzdrževanje",
|
"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.Reflection;
|
||||||
using System.Resources;
|
using System.Resources;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
// General Information about an assembly is controlled through the following
|
||||||
@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: AssemblyCulture("")]
|
[assembly: AssemblyCulture("")]
|
||||||
[assembly: NeutralResourcesLanguage("en")]
|
[assembly: NeutralResourcesLanguage("en")]
|
||||||
|
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
// 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
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<SessionEventArgs> SessionActivity;
|
public event EventHandler<SessionEventArgs> SessionActivity;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all connections.
|
/// Gets all connections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -312,6 +315,19 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnSessionControllerConnected(SessionInfo info)
|
||||||
|
{
|
||||||
|
EventHelper.QueueEventIfNotNull(
|
||||||
|
SessionControllerConnected,
|
||||||
|
this,
|
||||||
|
new SessionEventArgs
|
||||||
|
{
|
||||||
|
SessionInfo = info
|
||||||
|
},
|
||||||
|
_logger);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void CloseIfNeeded(SessionInfo session)
|
public void CloseIfNeeded(SessionInfo session)
|
||||||
{
|
{
|
||||||
|
@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
var controller = (WebSocketController)controllerInfo.Item1;
|
var controller = (WebSocketController)controllerInfo.Item1;
|
||||||
controller.AddWebSocket(connection);
|
controller.AddWebSocket(connection);
|
||||||
|
|
||||||
|
_sessionManager.OnSessionControllerConnected(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILibraryManager _libraryManager;
|
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>
|
/// <summary>
|
||||||
/// The map between sessions and groups.
|
/// The map between sessions and groups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
||||||
_sessionManager.SessionStarted += OnSessionManagerSessionStarted;
|
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
throw new InvalidOperationException("Could not add session to group!");
|
throw new InvalidOperationException("Could not add session to group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateSessionsCounter(session.UserId, 1);
|
||||||
group.CreateGroup(session, request, cancellationToken);
|
group.CreateGroup(session, request, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
if (existingGroup.GroupId.Equals(request.GroupId))
|
if (existingGroup.GroupId.Equals(request.GroupId))
|
||||||
{
|
{
|
||||||
// Restore session.
|
// Restore session.
|
||||||
|
UpdateSessionsCounter(session.UserId, 1);
|
||||||
group.SessionJoin(session, request, cancellationToken);
|
group.SessionJoin(session, request, cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
throw new InvalidOperationException("Could not add session to group!");
|
throw new InvalidOperationException("Could not add session to group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateSessionsCounter(session.UserId, 1);
|
||||||
group.SessionJoin(session, request, cancellationToken);
|
group.SessionJoin(session, request, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
throw new InvalidOperationException("Could not remove session from group!");
|
throw new InvalidOperationException("Could not remove session from group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateSessionsCounter(session.UserId, -1);
|
||||||
group.SessionLeave(session, request, cancellationToken);
|
group.SessionLeave(session, request, cancellationToken);
|
||||||
|
|
||||||
if (group.IsGroupEmpty())
|
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>
|
/// <summary>
|
||||||
/// Releases unmanaged and optionally managed resources.
|
/// Releases unmanaged and optionally managed resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
|
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
var session = e.SessionInfo;
|
var session = e.SessionInfo;
|
||||||
|
|
||||||
@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
JoinGroup(session, request, CancellationToken.None);
|
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.
|
// Don't add a package that doesn't have any compatible versions.
|
||||||
if (package.Versions.Count == 0)
|
if (package.versions.Count == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -555,6 +555,7 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
|
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
|
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
||||||
|
@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.SyncPlay;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
|
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
|
||||||
{
|
{
|
||||||
|
private readonly ISyncPlayManager _syncPlayManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
|
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
public SyncPlayAccessHandler(
|
public SyncPlayAccessHandler(
|
||||||
|
ISyncPlayManager syncPlayManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
INetworkManager networkManager,
|
INetworkManager networkManager,
|
||||||
IHttpContextAccessor httpContextAccessor)
|
IHttpContextAccessor httpContextAccessor)
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
{
|
{
|
||||||
|
_syncPlayManager = syncPlayManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
var userId = ClaimHelpers.GetUserId(context.User);
|
var userId = ClaimHelpers.GetUserId(context.User);
|
||||||
var user = _userManager.GetUserById(userId!.Value);
|
var user = _userManager.GetUserById(userId!.Value);
|
||||||
|
|
||||||
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess)
|
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|
||||||
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
|
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|
||||||
|
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
|
||||||
|
|| _syncPlayManager.IsUserActive(userId!.Value))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
|
||||||
|
{
|
||||||
|
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
|
||||||
|
{
|
||||||
|
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|
||||||
|
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
|
||||||
|
{
|
||||||
|
if (_syncPlayManager.IsUserActive(userId!.Value))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param>
|
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
|
||||||
public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess)
|
public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
|
||||||
{
|
{
|
||||||
RequiredAccess = requiredAccess;
|
RequiredAccess = requiredAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public SyncPlayAccessRequirement()
|
|
||||||
{
|
|
||||||
RequiredAccess = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the required SyncPlay access.
|
/// Gets the required SyncPlay access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SyncPlayAccess? RequiredAccess { get; }
|
public SyncPlayAccessRequirementType RequiredAccess { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
|
|||||||
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring access to SyncPlay.
|
/// Policy name for accessing SyncPlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayAccess = "SyncPlayAccess";
|
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring group creation access to SyncPlay.
|
/// Policy name for creating a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayCreateGroupAccess = "SyncPlayCreateGroupAccess";
|
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for joining a SyncPlay group.
|
||||||
|
/// </summary>
|
||||||
|
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for accessing a SyncPlay group.
|
||||||
|
/// </summary>
|
||||||
|
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public class DisplayPreferencesController : BaseJellyfinApiController
|
public class DisplayPreferencesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
||||||
|
private readonly ILogger<DisplayPreferencesController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
||||||
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
|
/// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
|
||||||
|
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
|
||||||
{
|
{
|
||||||
_displayPreferencesManager = displayPreferencesManager;
|
_displayPreferencesManager = displayPreferencesManager;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Client = displayPreferences.Client,
|
Client = displayPreferences.Client,
|
||||||
Id = displayPreferences.ItemId.ToString(),
|
Id = displayPreferences.ItemId.ToString(),
|
||||||
ViewType = itemPreferences.ViewType.ToString(),
|
|
||||||
SortBy = itemPreferences.SortBy,
|
SortBy = itemPreferences.SortBy,
|
||||||
SortOrder = itemPreferences.SortOrder,
|
SortOrder = itemPreferences.SortOrder,
|
||||||
IndexBy = displayPreferences.IndexBy?.ToString(),
|
IndexBy = displayPreferences.IndexBy?.ToString(),
|
||||||
@ -77,11 +80,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
|
|
||||||
{
|
|
||||||
dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
||||||
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
@ -189,10 +187,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId))
|
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||||
{
|
{
|
||||||
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client);
|
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
|
||||||
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
|
|
||||||
displayPreferences.CustomPrefs.Remove(key);
|
displayPreferences.CustomPrefs.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,11 +201,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
||||||
itemPrefs.ItemId = itemId;
|
itemPrefs.ItemId = itemId;
|
||||||
|
|
||||||
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
|
|
||||||
{
|
|
||||||
itemPrefs.ViewType = viewType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set all remaining custom preferences.
|
// Set all remaining custom preferences.
|
||||||
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
||||||
_displayPreferencesManager.SaveChanges();
|
_displayPreferencesManager.SaveChanges();
|
||||||
|
@ -98,7 +98,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
return Forbid("User is not allowed to update the image.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
return Forbid("User is not allowed to update the image.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
return Forbid("User is not allowed to delete the image.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
return Forbid("User is not allowed to delete the image.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
@ -119,7 +120,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? enableTranscoding,
|
[FromQuery] bool? enableTranscoding,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromBody] PlaybackInfoDto? playbackInfoDto)
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
|
||||||
{
|
{
|
||||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -17,6 +18,7 @@ using MediaBrowser.Model.Querying;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -53,6 +55,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new playlist.
|
/// Creates a new playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="name">The playlist name.</param>
|
||||||
|
/// <param name="ids">The item ids.</param>
|
||||||
|
/// <param name="userId">The user id.</param>
|
||||||
|
/// <param name="mediaType">The media type.</param>
|
||||||
/// <param name="createPlaylistRequest">The create playlist payload.</param>
|
/// <param name="createPlaylistRequest">The create playlist payload.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
|
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
|
||||||
@ -61,14 +70,23 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
||||||
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
|
[FromQuery] string? name,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] string? mediaType,
|
||||||
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
|
||||||
{
|
{
|
||||||
|
if (ids.Count == 0)
|
||||||
|
{
|
||||||
|
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
||||||
{
|
{
|
||||||
Name = createPlaylistRequest.Name,
|
Name = name ?? createPlaylistRequest?.Name,
|
||||||
ItemIdList = createPlaylistRequest.Ids,
|
ItemIdList = ids,
|
||||||
UserId = createPlaylistRequest.UserId,
|
UserId = userId ?? createPlaylistRequest?.UserId ?? default,
|
||||||
MediaType = createPlaylistRequest.MediaType
|
MediaType = mediaType ?? createPlaylistRequest?.MediaType
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (_quickConnect.State == QuickConnectState.Unavailable)
|
if (_quickConnect.State == QuickConnectState.Unavailable)
|
||||||
{
|
{
|
||||||
return Forbid("Quick connect is unavailable");
|
return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
_quickConnect.Activate();
|
_quickConnect.Activate();
|
||||||
@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||||
if (!userId.HasValue)
|
if (!userId.HasValue)
|
||||||
{
|
{
|
||||||
return Forbid("Unknown user id");
|
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
return _quickConnect.AuthorizeRequest(userId.Value, code);
|
return _quickConnect.AuthorizeRequest(userId.Value, code);
|
||||||
|
@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sync play controller.
|
/// The sync play controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
[Authorize(Policy = Policies.SyncPlayHasAccess)]
|
||||||
public class SyncPlayController : BaseJellyfinApiController
|
public class SyncPlayController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("New")]
|
[HttpPost("New")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
|
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
|
||||||
public ActionResult SyncPlayCreateGroup(
|
public ActionResult SyncPlayCreateGroup(
|
||||||
[FromBody, Required] NewGroupRequestDto requestData)
|
[FromBody, Required] NewGroupRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Join")]
|
[HttpPost("Join")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||||
public ActionResult SyncPlayJoinGroup(
|
public ActionResult SyncPlayJoinGroup(
|
||||||
[FromBody, Required] JoinGroupRequestDto requestData)
|
[FromBody, Required] JoinGroupRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Leave")]
|
[HttpPost("Leave")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayLeaveGroup()
|
public ActionResult SyncPlayLeaveGroup()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
|
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
|
||||||
[HttpGet("List")]
|
[HttpGet("List")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||||
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
|
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetNewQueue")]
|
[HttpPost("SetNewQueue")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetNewQueue(
|
public ActionResult SyncPlaySetNewQueue(
|
||||||
[FromBody, Required] PlayRequestDto requestData)
|
[FromBody, Required] PlayRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetPlaylistItem")]
|
[HttpPost("SetPlaylistItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetPlaylistItem(
|
public ActionResult SyncPlaySetPlaylistItem(
|
||||||
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("RemoveFromPlaylist")]
|
[HttpPost("RemoveFromPlaylist")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayRemoveFromPlaylist(
|
public ActionResult SyncPlayRemoveFromPlaylist(
|
||||||
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("MovePlaylistItem")]
|
[HttpPost("MovePlaylistItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayMovePlaylistItem(
|
public ActionResult SyncPlayMovePlaylistItem(
|
||||||
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Queue")]
|
[HttpPost("Queue")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayQueue(
|
public ActionResult SyncPlayQueue(
|
||||||
[FromBody, Required] QueueRequestDto requestData)
|
[FromBody, Required] QueueRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Unpause")]
|
[HttpPost("Unpause")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayUnpause()
|
public ActionResult SyncPlayUnpause()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Pause")]
|
[HttpPost("Pause")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayPause()
|
public ActionResult SyncPlayPause()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Stop")]
|
[HttpPost("Stop")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayStop()
|
public ActionResult SyncPlayStop()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Seek")]
|
[HttpPost("Seek")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySeek(
|
public ActionResult SyncPlaySeek(
|
||||||
[FromBody, Required] SeekRequestDto requestData)
|
[FromBody, Required] SeekRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Buffering")]
|
[HttpPost("Buffering")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayBuffering(
|
public ActionResult SyncPlayBuffering(
|
||||||
[FromBody, Required] BufferRequestDto requestData)
|
[FromBody, Required] BufferRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Ready")]
|
[HttpPost("Ready")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayReady(
|
public ActionResult SyncPlayReady(
|
||||||
[FromBody, Required] ReadyRequestDto requestData)
|
[FromBody, Required] ReadyRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetIgnoreWait")]
|
[HttpPost("SetIgnoreWait")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetIgnoreWait(
|
public ActionResult SyncPlaySetIgnoreWait(
|
||||||
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("NextItem")]
|
[HttpPost("NextItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayNextItem(
|
public ActionResult SyncPlayNextItem(
|
||||||
[FromBody, Required] NextItemRequestDto requestData)
|
[FromBody, Required] NextItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("PreviousItem")]
|
[HttpPost("PreviousItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayPreviousItem(
|
public ActionResult SyncPlayPreviousItem(
|
||||||
[FromBody, Required] PreviousItemRequestDto requestData)
|
[FromBody, Required] PreviousItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetRepeatMode")]
|
[HttpPost("SetRepeatMode")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetRepeatMode(
|
public ActionResult SyncPlaySetRepeatMode(
|
||||||
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
||||||
{
|
{
|
||||||
@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetShuffleMode")]
|
[HttpPost("SetShuffleMode")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetShuffleMode(
|
public ActionResult SyncPlaySetShuffleMode(
|
||||||
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
||||||
{
|
{
|
||||||
|
@ -133,11 +133,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult DeleteUser([FromRoute, Required] Guid userId)
|
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
_sessionManager.RevokeUserTokens(user.Id, null);
|
_sessionManager.RevokeUserTokens(user.Id, null);
|
||||||
_userManager.DeleteUser(userId);
|
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
|
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
|
||||||
{
|
{
|
||||||
return Forbid("Only sha1 password is not allowed.");
|
return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password should always be null
|
// Password should always be null
|
||||||
@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> UpdateUserPassword(
|
public async Task<ActionResult> UpdateUserPassword(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UpdateUserPassword request)
|
[FromBody, Required] UpdateUserPassword request)
|
||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
return Forbid("User is not allowed to update the password.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (success == null)
|
if (success == null)
|
||||||
{
|
{
|
||||||
return Forbid("Invalid user or password entered.");
|
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||||
@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult UpdateUserEasyPassword(
|
public ActionResult UpdateUserEasyPassword(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UpdateUserEasyPassword request)
|
[FromBody, Required] UpdateUserEasyPassword request)
|
||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
return Forbid("User is not allowed to update the easy password.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> UpdateUser(
|
public async Task<ActionResult> UpdateUser(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UserDto updateUser)
|
[FromBody, Required] UserDto updateUser)
|
||||||
{
|
{
|
||||||
if (updateUser == null)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
||||||
{
|
{
|
||||||
return Forbid("User update not allowed.");
|
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> UpdateUserPolicy(
|
public async Task<ActionResult> UpdateUserPolicy(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UserPolicy newPolicy)
|
[FromBody, Required] UserPolicy newPolicy)
|
||||||
{
|
{
|
||||||
if (newPolicy == null)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
|
||||||
// If removing admin access
|
// If removing admin access
|
||||||
@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
||||||
{
|
{
|
||||||
return Forbid("There must be at least one user in the system with administrative access.");
|
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If disabling
|
// If disabling
|
||||||
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
|
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
|
||||||
{
|
{
|
||||||
return Forbid("Administrators cannot be disabled.");
|
return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If disabling
|
// If disabling
|
||||||
@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
|
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
|
||||||
{
|
{
|
||||||
return Forbid("There must be at least one enabled user in the system.");
|
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
|
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
|
||||||
@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> UpdateUserConfiguration(
|
public async Task<ActionResult> UpdateUserConfiguration(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UserConfiguration userConfig)
|
[FromBody, Required] UserConfiguration userConfig)
|
||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
||||||
{
|
{
|
||||||
return Forbid("User configuration update not allowed");
|
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
|
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
|
||||||
@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost("New")]
|
[HttpPost("New")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
|
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
|
||||||
{
|
{
|
||||||
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
|
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges videos into a single record.
|
/// Merges videos into a single record.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param>
|
/// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
|
||||||
/// <response code="204">Videos merged.</response>
|
/// <response code="204">Videos merged.</response>
|
||||||
/// <response code="400">Supply at least 2 video ids.</response>
|
/// <response code="400">Supply at least 2 video ids.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
|
||||||
@ -204,9 +204,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
|
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||||
{
|
{
|
||||||
var items = itemIds
|
var items = ids
|
||||||
.Select(i => _libraryManager.GetItemById(i))
|
.Select(i => _libraryManager.GetItemById(i))
|
||||||
.OfType<Video>()
|
.OfType<Video>()
|
||||||
.OrderBy(i => i.Id)
|
.OrderBy(i => i.Id)
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||||
|
@ -24,7 +24,7 @@ namespace Jellyfin.Api.Models.PlaylistDtos
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user id.
|
/// Gets or sets the user id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the media type.
|
/// Gets or sets the media type.
|
||||||
|
@ -18,7 +18,8 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <param name="name">The name.</param>
|
/// <param name="name">The name.</param>
|
||||||
/// <param name="type">The type.</param>
|
/// <param name="type">The type.</param>
|
||||||
/// <param name="userId">The user id.</param>
|
/// <param name="userId">The user id.</param>
|
||||||
public ActivityLog(string name, string type, Guid userId)
|
/// <param name="logLevel">The log level.</param>
|
||||||
|
public ActivityLog(string name, string type, Guid userId, LogLevel logLevel = LogLevel.Information)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(name))
|
if (string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
@ -34,7 +35,7 @@ namespace Jellyfin.Data.Entities
|
|||||||
Type = type;
|
Type = type;
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
DateCreated = DateTime.UtcNow;
|
DateCreated = DateTime.UtcNow;
|
||||||
LogSeverity = LogLevel.Trace;
|
LogSeverity = logLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -23,7 +23,6 @@ namespace Jellyfin.Data.Entities
|
|||||||
Client = client;
|
Client = client;
|
||||||
|
|
||||||
SortBy = "SortName";
|
SortBy = "SortName";
|
||||||
ViewType = ViewType.Poster;
|
|
||||||
SortOrder = SortOrder.Ascending;
|
SortOrder = SortOrder.Ascending;
|
||||||
RememberSorting = false;
|
RememberSorting = false;
|
||||||
RememberIndexing = false;
|
RememberIndexing = false;
|
||||||
|
@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
|
|||||||
EnableAutoLogin = false;
|
EnableAutoLogin = false;
|
||||||
PlayDefaultAudioTrack = true;
|
PlayDefaultAudioTrack = true;
|
||||||
SubtitleMode = SubtitlePlaybackMode.Default;
|
SubtitleMode = SubtitlePlaybackMode.Default;
|
||||||
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
|
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
|
||||||
|
|
||||||
AddDefaultPermissions();
|
AddDefaultPermissions();
|
||||||
AddDefaultPreferences();
|
AddDefaultPreferences();
|
||||||
@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the level of sync play permissions this user has.
|
/// Gets or sets the level of sync play permissions this user has.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SyncPlayAccess SyncPlayAccess { get; set; }
|
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the row version.
|
/// Gets or sets the row version.
|
||||||
|
28
Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs
Normal file
28
Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enum SyncPlayAccessRequirementType.
|
||||||
|
/// </summary>
|
||||||
|
public enum SyncPlayAccessRequirementType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User must have access to SyncPlay, in some form.
|
||||||
|
/// </summary>
|
||||||
|
HasAccess = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User must be able to create groups.
|
||||||
|
/// </summary>
|
||||||
|
CreateGroup = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User must be able to join groups.
|
||||||
|
/// </summary>
|
||||||
|
JoinGroup = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User must be in a group.
|
||||||
|
/// </summary>
|
||||||
|
IsInGroup = 3
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
namespace Jellyfin.Data.Enums
|
namespace Jellyfin.Data.Enums
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enum SyncPlayAccess.
|
/// Enum SyncPlayUserAccessType.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SyncPlayAccess
|
public enum SyncPlayUserAccessType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User can create groups and join them.
|
/// User can create groups and join them.
|
@ -1,4 +1,4 @@
|
|||||||
namespace Jellyfin.Data.Enums
|
namespace Jellyfin.Data.Enums
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An enum representing the type of view for a library or collection.
|
/// An enum representing the type of view for a library or collection.
|
||||||
@ -6,33 +6,108 @@
|
|||||||
public enum ViewType
|
public enum ViewType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows banners.
|
/// Shows albums.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Banner = 0,
|
Albums = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows a list of content.
|
/// Shows album artists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
List = 1,
|
AlbumArtists = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows poster artwork.
|
/// Shows artists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Poster = 2,
|
Artists = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows poster artwork with a card containing the name and year.
|
/// Shows channels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PosterCard = 3,
|
Channels = 3,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows a thumbnail.
|
/// Shows collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Thumb = 4,
|
Collections = 4,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows a thumbnail with a card containing the name and year.
|
/// Shows episodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ThumbCard = 5
|
Episodes = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows favorites.
|
||||||
|
/// </summary>
|
||||||
|
Favorites = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows genres.
|
||||||
|
/// </summary>
|
||||||
|
Genres = 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows guide.
|
||||||
|
/// </summary>
|
||||||
|
Guide = 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows movies.
|
||||||
|
/// </summary>
|
||||||
|
Movies = 9,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows networks.
|
||||||
|
/// </summary>
|
||||||
|
Networks = 10,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows playlists.
|
||||||
|
/// </summary>
|
||||||
|
Playlists = 11,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows programs.
|
||||||
|
/// </summary>
|
||||||
|
Programs = 12,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows recordings.
|
||||||
|
/// </summary>
|
||||||
|
Recordings = 13,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows schedule.
|
||||||
|
/// </summary>
|
||||||
|
Schedule = 14,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows series.
|
||||||
|
/// </summary>
|
||||||
|
Series = 15,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows shows.
|
||||||
|
/// </summary>
|
||||||
|
Shows = 16,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows songs.
|
||||||
|
/// </summary>
|
||||||
|
Songs = 17,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows songs.
|
||||||
|
/// </summary>
|
||||||
|
Suggestions = 18,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows trailers.
|
||||||
|
/// </summary>
|
||||||
|
Trailers = 19,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows upcoming.
|
||||||
|
/// </summary>
|
||||||
|
Upcoming = 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -435,7 +435,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
0f,
|
0f,
|
||||||
kernelOffset,
|
kernelOffset,
|
||||||
SKShaderTileMode.Clamp,
|
SKShaderTileMode.Clamp,
|
||||||
false);
|
true);
|
||||||
|
|
||||||
canvas.DrawBitmap(
|
canvas.DrawBitmap(
|
||||||
source,
|
source,
|
||||||
|
@ -27,6 +27,16 @@ namespace Jellyfin.Networking.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool RequireHttps { get; set; }
|
public bool RequireHttps { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
|
||||||
|
/// </summary>
|
||||||
|
public string CertificatePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string CertificatePassword { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
|
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -83,7 +93,7 @@ namespace Jellyfin.Networking.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
|
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
|
||||||
/// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>.
|
/// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool EnableHttps { get; set; }
|
public bool EnableHttps { get; set; }
|
||||||
|
|
||||||
|
@ -1314,9 +1314,7 @@ namespace Jellyfin.Networking.Manager
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Have to return something, so return an internal address
|
_logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source);
|
||||||
|
|
||||||
_logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Activity
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
|
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task CreateAsync(ActivityLog entry)
|
public async Task CreateAsync(ActivityLog entry)
|
||||||
|
@ -86,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetPlaybackNotificationType(string mediaType)
|
private static string? GetPlaybackNotificationType(string mediaType)
|
||||||
{
|
{
|
||||||
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -94,7 +94,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetPlaybackStoppedNotificationType(string mediaType)
|
private static string? GetPlaybackStoppedNotificationType(string mediaType)
|
||||||
{
|
{
|
||||||
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
@ -25,11 +26,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#nullable disable
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
#pragma warning disable CA1307
|
||||||
#pragma warning disable CA1307
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@ -220,7 +219,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void DeleteUser(Guid userId)
|
public async Task DeleteUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
if (!_users.TryGetValue(userId, out var user))
|
if (!_users.TryGetValue(userId, out var user))
|
||||||
{
|
{
|
||||||
@ -246,7 +245,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
nameof(userId));
|
nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateContext();
|
await using var dbContext = _dbProvider.CreateContext();
|
||||||
|
|
||||||
// Clear all entities related to the user from the database.
|
// Clear all entities related to the user from the database.
|
||||||
if (user.ProfileImage != null)
|
if (user.ProfileImage != null)
|
||||||
@ -258,10 +257,10 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
dbContext.RemoveRange(user.Preferences);
|
dbContext.RemoveRange(user.Preferences);
|
||||||
dbContext.RemoveRange(user.AccessSchedules);
|
dbContext.RemoveRange(user.AccessSchedules);
|
||||||
dbContext.Users.Remove(user);
|
dbContext.Users.Remove(user);
|
||||||
dbContext.SaveChanges();
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
_users.Remove(userId);
|
_users.Remove(userId);
|
||||||
|
|
||||||
_eventManager.Publish(new UserDeletedEventArgs(user));
|
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@ -13,7 +13,7 @@ namespace Jellyfin.Server.Implementations.ValueConverters
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="kind">The kind to specify.</param>
|
/// <param name="kind">The kind to specify.</param>
|
||||||
/// <param name="mappingHints">The mapping hints.</param>
|
/// <param name="mappingHints">The mapping hints.</param>
|
||||||
public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null)
|
public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints? mappingHints = null)
|
||||||
: base(v => v.ToUniversalTime(), v => DateTime.SpecifyKind(v, kind), mappingHints)
|
: base(v => v.ToUniversalTime(), v => DateTime.SpecifyKind(v, kind), mappingHints)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -107,5 +107,28 @@ namespace Jellyfin.Server.Extensions
|
|||||||
{
|
{
|
||||||
return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
|
return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds robots.txt redirection to the application pipeline.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appBuilder">The application builder.</param>
|
||||||
|
/// <returns>The updated application builder.</returns>
|
||||||
|
public static IApplicationBuilder UseRobotsRedirection(this IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds /emby and /mediabrowser route trimming to the application pipeline.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This must be injected before any path related middleware.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="appBuilder">The application builder.</param>
|
||||||
|
/// <returns>The updated application builder.</returns>
|
||||||
|
public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ using Jellyfin.Server.Configuration;
|
|||||||
using Jellyfin.Server.Filters;
|
using Jellyfin.Server.Filters;
|
||||||
using Jellyfin.Server.Formatters;
|
using Jellyfin.Server.Formatters;
|
||||||
using MediaBrowser.Common.Json;
|
using MediaBrowser.Common.Json;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -127,18 +128,32 @@ namespace Jellyfin.Server.Extensions
|
|||||||
policy.AddRequirements(new RequiresElevationRequirement());
|
policy.AddRequirements(new RequiresElevationRequirement());
|
||||||
});
|
});
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.SyncPlayAccess,
|
Policies.SyncPlayHasAccess,
|
||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups));
|
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
|
||||||
});
|
});
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.SyncPlayCreateGroupAccess,
|
Policies.SyncPlayCreateGroup,
|
||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.CreateAndJoinGroups));
|
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
|
||||||
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.SyncPlayJoinGroup,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
|
||||||
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.SyncPlayIsInGroup,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -169,11 +184,19 @@ namespace Jellyfin.Server.Extensions
|
|||||||
.Configure<ForwardedHeadersOptions>(options =>
|
.Configure<ForwardedHeadersOptions>(options =>
|
||||||
{
|
{
|
||||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
for (var i = 0; i < knownProxies.Count; i++)
|
if (knownProxies.Count == 0)
|
||||||
{
|
{
|
||||||
if (IPAddress.TryParse(knownProxies[i], out var address))
|
options.KnownNetworks.Clear();
|
||||||
|
options.KnownProxies.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < knownProxies.Count; i++)
|
||||||
{
|
{
|
||||||
options.KnownProxies.Add(address);
|
if (IPHost.TryParse(knownProxies[i], out var host))
|
||||||
|
{
|
||||||
|
options.KnownProxies.Add(host.Address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -14,7 +14,8 @@ namespace Jellyfin.Server.Filters
|
|||||||
{
|
{
|
||||||
Schema = new OpenApiSchema
|
Schema = new OpenApiSchema
|
||||||
{
|
{
|
||||||
Type = "file"
|
Type = "string",
|
||||||
|
Format = "binary"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.1" />
|
||||||
<PackageReference Include="prometheus-net" Version="4.0.0" />
|
<PackageReference Include="prometheus-net" Version="4.0.0" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Middleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Removes /emby and /mediabrowser from requested route.
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyEmbyRouteRewriteMiddleware
|
||||||
|
{
|
||||||
|
private const string EmbyPath = "/emby";
|
||||||
|
private const string MediabrowserPath = "/mediabrowser";
|
||||||
|
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">The next delegate in the pipeline.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public LegacyEmbyRouteRewriteMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the middleware action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
|
/// <returns>The async task.</returns>
|
||||||
|
public async Task Invoke(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var localPath = httpContext.Request.Path.ToString();
|
||||||
|
if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
httpContext.Request.Path = localPath[EmbyPath.Length..];
|
||||||
|
_logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
|
||||||
|
}
|
||||||
|
else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
httpContext.Request.Path = localPath[MediabrowserPath.Length..];
|
||||||
|
_logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
Normal file
47
Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Middleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Redirect requests to robots.txt to web/robots.txt.
|
||||||
|
/// </summary>
|
||||||
|
public class RobotsRedirectionMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<RobotsRedirectionMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">The next delegate in the pipeline.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public RobotsRedirectionMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
ILogger<RobotsRedirectionMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the middleware action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
|
/// <returns>The async task.</returns>
|
||||||
|
public async Task Invoke(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var localPath = httpContext.Request.Path.ToString();
|
||||||
|
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
|
||||||
|
httpContext.Response.Redirect("web/robots.txt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -81,6 +81,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
{ "unstable", ChromecastVersion.Unstable }
|
{ "unstable", ChromecastVersion.Unstable }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var customDisplayPrefs = new HashSet<string>();
|
||||||
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
||||||
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
|
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
|
||||||
{
|
{
|
||||||
@ -185,7 +186,13 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
|
|
||||||
foreach (var (key, value) in dto.CustomPrefs)
|
foreach (var (key, value) in dto.CustomPrefs)
|
||||||
{
|
{
|
||||||
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
|
// Custom display preferences can have a key collision.
|
||||||
|
var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}";
|
||||||
|
if (!customDisplayPrefs.Contains(indexKey))
|
||||||
|
{
|
||||||
|
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
|
||||||
|
customDisplayPrefs.Add(indexKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.Add(displayPreferences);
|
dbContext.Add(displayPreferences);
|
||||||
|
@ -128,6 +128,8 @@ namespace Jellyfin.Server
|
|||||||
mainApp.UseHttpsRedirection();
|
mainApp.UseHttpsRedirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This must be injected before any path related middleware.
|
||||||
|
mainApp.UsePathTrim();
|
||||||
mainApp.UseStaticFiles();
|
mainApp.UseStaticFiles();
|
||||||
if (appConfig.HostWebClient())
|
if (appConfig.HostWebClient())
|
||||||
{
|
{
|
||||||
@ -142,6 +144,8 @@ namespace Jellyfin.Server
|
|||||||
RequestPath = "/web",
|
RequestPath = "/web",
|
||||||
ContentTypeProvider = extensionProvider
|
ContentTypeProvider = extensionProvider
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mainApp.UseRobotsRedirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
mainApp.UseAuthentication();
|
mainApp.UseAuthentication();
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Common.Json.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a number to a boolean.
|
||||||
|
/// This is needed for HDHomerun.
|
||||||
|
/// </summary>
|
||||||
|
public class JsonBoolNumberConverter : JsonConverter<bool>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
return Convert.ToBoolean(reader.GetInt32());
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.GetBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteBooleanValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
@ -13,21 +14,13 @@ namespace MediaBrowser.Common.Json.Converters
|
|||||||
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var guidStr = reader.GetString();
|
var guidStr = reader.GetString();
|
||||||
|
|
||||||
return guidStr == null ? Guid.Empty : new Guid(guidStr);
|
return guidStr == null ? Guid.Empty : new Guid(guidStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value == Guid.Empty)
|
writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ namespace MediaBrowser.Common.Json
|
|||||||
options.Converters.Add(new JsonVersionConverter());
|
options.Converters.Add(new JsonVersionConverter());
|
||||||
options.Converters.Add(new JsonStringEnumConverter());
|
options.Converters.Add(new JsonStringEnumConverter());
|
||||||
options.Converters.Add(new JsonNullableStructConverterFactory());
|
options.Converters.Add(new JsonNullableStructConverterFactory());
|
||||||
|
options.Converters.Add(new JsonBoolNumberConverter());
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
@ -48,10 +48,10 @@ namespace MediaBrowser.Controller.BaseItemManager
|
|||||||
return !baseItem.EnableMediaSourceDisplay;
|
return !baseItem.EnableMediaSourceDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
|
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
|
||||||
if (typeOptions != null)
|
if (typeOptions != null)
|
||||||
{
|
{
|
||||||
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!libraryOptions.EnableInternetProviders)
|
if (!libraryOptions.EnableInternetProviders)
|
||||||
@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.BaseItemManager
|
|||||||
|
|
||||||
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
|
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.BaseItemManager
|
|||||||
return !baseItem.EnableMediaSourceDisplay;
|
return !baseItem.EnableMediaSourceDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
|
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
|
||||||
if (typeOptions != null)
|
if (typeOptions != null)
|
||||||
{
|
{
|
||||||
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
@ -1385,6 +1385,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
new List<FileSystemMetadata>();
|
new List<FileSystemMetadata>();
|
||||||
|
|
||||||
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
|
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
|
||||||
|
await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
|
||||||
|
|
||||||
if (ownedItemsChanged)
|
if (ownedItemsChanged)
|
||||||
{
|
{
|
||||||
|
@ -354,6 +354,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// metadata is up-to-date; make sure DB has correct images dimensions and hash
|
||||||
|
await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -571,7 +571,7 @@ namespace MediaBrowser.Controller.Library
|
|||||||
string videoPath,
|
string videoPath,
|
||||||
string[] files);
|
string[] files);
|
||||||
|
|
||||||
void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason);
|
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
|
||||||
|
|
||||||
BaseItem GetParentItem(string parentId, Guid? userId);
|
BaseItem GetParentItem(string parentId, Guid? userId);
|
||||||
|
|
||||||
|
@ -93,7 +93,8 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// Deletes the specified user.
|
/// Deletes the specified user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The id of the user to be deleted.</param>
|
/// <param name="userId">The id of the user to be deleted.</param>
|
||||||
void DeleteUser(Guid userId);
|
/// <returns>A task representing the deletion of the user.</returns>
|
||||||
|
Task DeleteUserAsync(Guid userId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets the password.
|
/// Resets the password.
|
||||||
|
@ -46,6 +46,11 @@ namespace MediaBrowser.Controller.Session
|
|||||||
|
|
||||||
event EventHandler<SessionEventArgs> SessionActivity;
|
event EventHandler<SessionEventArgs> SessionActivity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when [session controller connected].
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [capabilities changed].
|
/// Occurs when [capabilities changed].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -78,6 +83,12 @@ namespace MediaBrowser.Controller.Session
|
|||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
|
SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to report that a session controller has connected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="session">The session.</param>
|
||||||
|
void OnSessionControllerConnected(SessionInfo session);
|
||||||
|
|
||||||
void UpdateDeviceName(string sessionId, string reportedDeviceName);
|
void UpdateDeviceName(string sessionId, string reportedDeviceName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -51,5 +51,12 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <param name="request">The request.</param>
|
/// <param name="request">The request.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken);
|
void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a user has an active session using SyncPlay.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user identifier to check.</param>
|
||||||
|
/// <returns><c>true</c> if the user is using SyncPlay; <c>false</c> otherwise.</returns>
|
||||||
|
bool IsUserActive(Guid userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -603,16 +603,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
|
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
|
||||||
|
// mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will lower the processing speed.
|
||||||
var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
|
var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
|
||||||
if (enableThumbnail)
|
if (enableThumbnail)
|
||||||
{
|
{
|
||||||
|
var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var batchSize = useLargerBatchSize ? "50" : "24";
|
||||||
if (string.IsNullOrEmpty(vf))
|
if (string.IsNullOrEmpty(vf))
|
||||||
{
|
{
|
||||||
vf = "-vf thumbnail=24";
|
vf = "-vf thumbnail=" + batchSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
vf += ",thumbnail=24";
|
vf += ",thumbnail=" + batchSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,11 +88,11 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
// The left side of the dot is the platform number, and the right side is the device number on the platform.
|
// The left side of the dot is the platform number, and the right side is the device number on the platform.
|
||||||
OpenclDevice = "0.0";
|
OpenclDevice = "0.0";
|
||||||
EnableTonemapping = false;
|
EnableTonemapping = false;
|
||||||
TonemappingAlgorithm = "reinhard";
|
TonemappingAlgorithm = "hable";
|
||||||
TonemappingRange = "auto";
|
TonemappingRange = "auto";
|
||||||
TonemappingDesat = 0;
|
TonemappingDesat = 0;
|
||||||
TonemappingThreshold = 0.8;
|
TonemappingThreshold = 0.8;
|
||||||
TonemappingPeak = 0;
|
TonemappingPeak = 100;
|
||||||
TonemappingParam = 0;
|
TonemappingParam = 0;
|
||||||
H264Crf = 23;
|
H264Crf = 23;
|
||||||
H265Crf = 28;
|
H265Crf = 28;
|
||||||
|
@ -152,7 +152,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the channel identifier.
|
/// Gets or sets the channel identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The channel identifier.</value>
|
/// <value>The channel identifier.</value>
|
||||||
public Guid ChannelId { get; set; }
|
public Guid? ChannelId { get; set; }
|
||||||
|
|
||||||
public string ChannelName { get; set; }
|
public string ChannelName { get; set; }
|
||||||
|
|
||||||
@ -270,7 +270,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the parent id.
|
/// Gets or sets the parent id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The parent id.</value>
|
/// <value>The parent id.</value>
|
||||||
public Guid ParentId { get; set; }
|
public Guid? ParentId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type.
|
/// Gets or sets the type.
|
||||||
@ -344,13 +344,13 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the series id.
|
/// Gets or sets the series id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The series id.</value>
|
/// <value>The series id.</value>
|
||||||
public Guid SeriesId { get; set; }
|
public Guid? SeriesId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the season identifier.
|
/// Gets or sets the season identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The season identifier.</value>
|
/// <value>The season identifier.</value>
|
||||||
public Guid SeasonId { get; set; }
|
public Guid? SeasonId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the special feature count.
|
/// Gets or sets the special feature count.
|
||||||
@ -428,7 +428,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the album id.
|
/// Gets or sets the album id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The album id.</value>
|
/// <value>The album id.</value>
|
||||||
public Guid AlbumId { get; set; }
|
public Guid? AlbumId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the album image tag.
|
/// Gets or sets the album image tag.
|
||||||
|
@ -111,7 +111,7 @@ namespace MediaBrowser.Model.Users
|
|||||||
/// Gets or sets a value indicating what SyncPlay features the user can access.
|
/// Gets or sets a value indicating what SyncPlay features the user can access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>Access level to SyncPlay features.</value>
|
/// <value>Access level to SyncPlay features.</value>
|
||||||
public SyncPlayAccess SyncPlayAccess { get; set; }
|
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
|
||||||
|
|
||||||
public UserPolicy()
|
public UserPolicy()
|
||||||
{
|
{
|
||||||
@ -160,7 +160,7 @@ namespace MediaBrowser.Model.Users
|
|||||||
EnableContentDownloading = true;
|
EnableContentDownloading = true;
|
||||||
EnablePublicSharing = true;
|
EnablePublicSharing = true;
|
||||||
EnableRemoteAccess = true;
|
EnableRemoteAccess = true;
|
||||||
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
|
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
|
private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var personsToSave = new List<BaseItem>();
|
var personsToSave = new List<BaseItem>();
|
||||||
|
|
||||||
@ -239,6 +239,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
||||||
{
|
{
|
||||||
|
var itemUpdateType = ItemUpdateType.MetadataDownload;
|
||||||
var saveEntity = false;
|
var saveEntity = false;
|
||||||
var personEntity = LibraryManager.GetPerson(person.Name);
|
var personEntity = LibraryManager.GetPerson(person.Name);
|
||||||
foreach (var id in person.ProviderIds)
|
foreach (var id in person.ProviderIds)
|
||||||
@ -261,18 +262,18 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
0);
|
0);
|
||||||
|
|
||||||
saveEntity = true;
|
saveEntity = true;
|
||||||
|
itemUpdateType = ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveEntity)
|
if (saveEntity)
|
||||||
{
|
{
|
||||||
personsToSave.Add(personEntity);
|
personsToSave.Add(personEntity);
|
||||||
|
await LibraryManager.RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload);
|
|
||||||
LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
|
LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||||
|
@ -425,7 +425,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||||||
{
|
{
|
||||||
var person = new PersonInfo
|
var person = new PersonInfo
|
||||||
{
|
{
|
||||||
Name = result.Director.Trim(),
|
Name = result.Writer.Trim(),
|
||||||
Type = PersonType.Writer
|
Type = PersonType.Writer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,12 +105,6 @@ There are three options to get the files for the web client.
|
|||||||
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
|
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
|
||||||
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
|
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
|
||||||
|
|
||||||
Once you have a copy of the built web client files, you need to copy them into a specific directory.
|
|
||||||
|
|
||||||
> `<repository root>/Mediabrowser.WebDashboard/jellyfin-web`
|
|
||||||
|
|
||||||
As part of the build process, this folder will be copied to the build output directory, where it can be accessed by the server.
|
|
||||||
|
|
||||||
### Running The Server
|
### Running The Server
|
||||||
|
|
||||||
The following instructions will help you get the project up and running via the command line, or your preferred IDE.
|
The following instructions will help you get the project up and running via the command line, or your preferred IDE.
|
||||||
@ -133,7 +127,7 @@ To run the server from the command line you can use the `dotnet run` command. Th
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd jellyfin # Move into the repository directory
|
cd jellyfin # Move into the repository directory
|
||||||
dotnet run --project Jellyfin.Server # Run the server startup project
|
dotnet run --project Jellyfin.Server --webdir /absolute/path/to/jellyfin-web/dist # Run the server startup project
|
||||||
```
|
```
|
||||||
|
|
||||||
A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options.
|
A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options.
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -15,7 +15,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -15,7 +15,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
# Install dotnet repository
|
# Install dotnet repository
|
||||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||||
&& mkdir -p dotnet-sdk \
|
&& mkdir -p dotnet-sdk \
|
||||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
<PackageReference Include="AutoFixture" Version="4.14.0" />
|
<PackageReference Include="AutoFixture" Version="4.14.0" />
|
||||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
|
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
|
||||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
|
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||||
|
34
tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
Normal file
34
tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using MediaBrowser.Common.Json.Converters;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Common.Tests.Json
|
||||||
|
{
|
||||||
|
public static class JsonBoolNumberTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("1", true)]
|
||||||
|
[InlineData("0", false)]
|
||||||
|
[InlineData("2", true)]
|
||||||
|
[InlineData("true", true)]
|
||||||
|
[InlineData("false", false)]
|
||||||
|
public static void Deserialize_Number_Valid_Success(string input, bool? output)
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions();
|
||||||
|
options.Converters.Add(new JsonBoolNumberConverter());
|
||||||
|
var value = JsonSerializer.Deserialize<bool>(input, options);
|
||||||
|
Assert.Equal(value, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, "true")]
|
||||||
|
[InlineData(false, "false")]
|
||||||
|
public static void Serialize_Bool_Success(bool input, string output)
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions();
|
||||||
|
options.Converters.Add(new JsonBoolNumberConverter());
|
||||||
|
var value = JsonSerializer.Serialize(input, options);
|
||||||
|
Assert.Equal(value, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MediaBrowser.Common.Json.Converters;
|
using MediaBrowser.Common.Json.Converters;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Jellyfin.Common.Tests.Extensions
|
namespace Jellyfin.Common.Tests.Json
|
||||||
{
|
{
|
||||||
public class JsonGuidConverterTests
|
public class JsonGuidConverterTests
|
||||||
{
|
{
|
||||||
@ -44,9 +45,25 @@ namespace Jellyfin.Common.Tests.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Serialize_EmptyGuid_Null()
|
public void Serialize_EmptyGuid_EmptyGuid()
|
||||||
{
|
{
|
||||||
Assert.Equal("null", JsonSerializer.Serialize(Guid.Empty, _options));
|
Assert.Equal($"\"{Guid.Empty:N}\"", JsonSerializer.Serialize(Guid.Empty, _options));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_Valid_NoDash_Success()
|
||||||
|
{
|
||||||
|
var guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
|
||||||
|
var str = JsonSerializer.Serialize(guid, _options);
|
||||||
|
Assert.Equal($"\"{guid:N}\"", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_Nullable_Success()
|
||||||
|
{
|
||||||
|
Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
|
||||||
|
var str = JsonSerializer.Serialize(guid, _options);
|
||||||
|
Assert.Equal($"\"{guid:N}\"", str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user