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];
|
||||
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++)
|
||||
{
|
||||
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);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -275,13 +276,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||
|
||||
CertificateInfo = new CertificateInfo
|
||||
{
|
||||
Path = ServerConfigurationManager.Configuration.CertificatePath,
|
||||
Password = ServerConfigurationManager.Configuration.CertificatePassword
|
||||
};
|
||||
Certificate = GetCertificate(CertificateInfo);
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||
|
@ -496,6 +490,7 @@ namespace Emby.Server.Implementations
|
|||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||
|
||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||
|
||||
_mediaEncoder.SetFFmpegPath();
|
||||
|
||||
|
@ -545,6 +540,13 @@ namespace Emby.Server.Implementations
|
|||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||
}
|
||||
|
||||
CertificateInfo = new CertificateInfo
|
||||
{
|
||||
Path = networkConfiguration.CertificatePath,
|
||||
Password = networkConfiguration.CertificatePassword
|
||||
};
|
||||
Certificate = GetCertificate(CertificateInfo);
|
||||
|
||||
DiscoverTypes();
|
||||
|
||||
RegisterServices();
|
||||
|
@ -754,7 +756,7 @@ namespace Emby.Server.Implementations
|
|||
// Don't use an empty string password
|
||||
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
|
||||
|
||||
var localCert = new X509Certificate2(certificateLocation, password);
|
||||
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
|
||||
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
||||
if (!localCert.HasPrivateKey)
|
||||
{
|
||||
|
@ -911,11 +913,11 @@ namespace Emby.Server.Implementations
|
|||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var requiresRestart = false;
|
||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||
|
||||
// Don't do anything if these haven't been set yet
|
||||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
{
|
||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
|
@ -935,10 +937,7 @@ namespace Emby.Server.Implementations
|
|||
requiresRestart = true;
|
||||
}
|
||||
|
||||
var currentCertPath = CertificateInfo?.Path;
|
||||
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
|
||||
|
||||
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
|
||||
if (ValidateSslCertificate(networkConfiguration))
|
||||
{
|
||||
requiresRestart = true;
|
||||
}
|
||||
|
@ -951,6 +950,33 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SSL certificate.
|
||||
/// </summary>
|
||||
/// <param name="networkConfig">The new configuration.</param>
|
||||
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
|
||||
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
|
||||
{
|
||||
var newPath = networkConfig.CertificatePath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
if (File.Exists(newPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Certificate file '{0}' does not exist.",
|
||||
newPath));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||
/// </summary>
|
||||
|
|
|
@ -88,38 +88,12 @@ namespace Emby.Server.Implementations.Configuration
|
|||
var newConfig = (ServerConfiguration)newConfiguration;
|
||||
|
||||
ValidateMetadataPath(newConfig);
|
||||
ValidateSslCertificate(newConfig);
|
||||
|
||||
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
||||
|
||||
base.ReplaceConfiguration(newConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SSL certificate.
|
||||
/// </summary>
|
||||
/// <param name="newConfig">The new configuration.</param>
|
||||
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
|
||||
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
|
||||
{
|
||||
var serverConfig = (ServerConfiguration)newConfig;
|
||||
|
||||
var newPath = serverConfig.CertificatePath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
if (!File.Exists(newPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Certificate file '{0}' does not exist.",
|
||||
newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the metadata path.
|
||||
/// </summary>
|
||||
|
|
|
@ -1138,7 +1138,10 @@ namespace Emby.Server.Implementations.Dto
|
|||
if (episodeSeries != null)
|
||||
{
|
||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
{
|
||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1185,7 +1188,10 @@ namespace Emby.Server.Implementations.Dto
|
|||
if (series != null)
|
||||
{
|
||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
||||
AttachPrimaryImageAspectRatio(dto, series);
|
||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||
{
|
||||
AttachPrimaryImageAspectRatio(dto, series);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
|
|
|
@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
updateToken = true;
|
||||
}
|
||||
|
||||
authInfo.IsApiKey = true;
|
||||
authInfo.IsApiKey = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
authInfo.IsApiKey = false;
|
||||
authInfo.IsApiKey = true;
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// A library post scan/refresh task for pre-fetching remote images.
|
||||
/// </summary>
|
||||
public class ImageFetcherPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ILogger<ImageFetcherPostScanTask> _logger;
|
||||
private readonly SemaphoreSlim _imageFetcherLock;
|
||||
|
||||
private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
|
||||
/// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
|
||||
public ImageFetcherPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
IProviderManager providerManager,
|
||||
ILogger<ImageFetcherPostScanTask> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_providerManager = providerManager;
|
||||
_logger = logger;
|
||||
_queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
|
||||
_imageFetcherLock = new SemaphoreSlim(1, 1);
|
||||
_libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
|
||||
_libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
|
||||
_providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
// Sometimes a library scan will cause this to run twice if there's an item refresh going on.
|
||||
await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var itemGuids = _queuedItems.Keys.ToList();
|
||||
|
||||
for (var i = 0; i < itemGuids.Count; i++)
|
||||
{
|
||||
if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
var itemType = queuedItem.item.GetType();
|
||||
_logger.LogDebug(
|
||||
"Updating remote images for item {ItemId} with media type {ItemMediaType}",
|
||||
itemId,
|
||||
itemType);
|
||||
try
|
||||
{
|
||||
await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
|
||||
}
|
||||
|
||||
_queuedItems.TryRemove(queuedItem.item.Id, out _);
|
||||
}
|
||||
|
||||
if (itemGuids.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
|
||||
itemGuids.Count.ToString(CultureInfo.InvariantCulture),
|
||||
(DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No images were updated.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_imageFetcherLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
|
||||
{
|
||||
if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
|
||||
{
|
||||
_queuedItems.AddOrUpdate(
|
||||
itemChangeEventArgs.Item.Id,
|
||||
(itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
|
||||
(key, existingValue) => existingValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
|
||||
{
|
||||
_queuedItems.AddOrUpdate(
|
||||
e.Argument.Id,
|
||||
(e.Argument, ItemUpdateType.None),
|
||||
(key, existingValue) => existingValue);
|
||||
}
|
||||
|
||||
// The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
|
||||
// the item that was refreshed regardless of children refreshes. So we take it as a signal
|
||||
// that the refresh is entirely completed.
|
||||
Run(null, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,7 +42,6 @@ using MediaBrowser.Model.Dto;
|
|||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Providers.MediaInfo;
|
||||
|
@ -1955,9 +1954,12 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
RunMetadataSavers(items, updateReason);
|
||||
foreach (var item in items)
|
||||
{
|
||||
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
|
@ -1988,25 +1990,22 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
|
||||
|
||||
public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason)
|
||||
public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||
{
|
||||
foreach (var item in items)
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
ProviderManager.SaveMetadata(item, updateReason);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
ProviderManager.SaveMetadata(item, updateReason);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||
{
|
||||
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
|
||||
{
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||
|
||||
protected override Book Resolve(ItemResolveArgs args)
|
||||
{
|
||||
|
|
|
@ -139,13 +139,13 @@ namespace Emby.Server.Implementations.Library
|
|||
return list
|
||||
.OrderBy(i =>
|
||||
{
|
||||
var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
var index = orders.IndexOf(i.Id.ToString("D", CultureInfo.InvariantCulture));
|
||||
|
||||
if (index == -1
|
||||
&& i is UserView view
|
||||
&& view.DisplayParentId != Guid.Empty)
|
||||
{
|
||||
index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture));
|
||||
index = orders.IndexOf(view.DisplayParentId.ToString("D", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return index == -1 ? int.MaxValue : index;
|
||||
|
|
|
@ -611,25 +611,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
CancellationToken cancellationToken,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
try
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_tokens.Clear();
|
||||
|
||||
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
|
||||
{
|
||||
enableRetry = false;
|
||||
}
|
||||
|
||||
if (!enableRetry)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Response is automatically disposed in the calling function,
|
||||
// so dispose manually if not returning.
|
||||
response.Dispose();
|
||||
if (!enableRetry || (int)response.StatusCode >= 500)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
_tokens.Clear();
|
||||
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
|
||||
return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -647,6 +647,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
|
||||
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
||||
|
@ -701,6 +702,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
try
|
||||
{
|
||||
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var response = httpResponse.Content;
|
||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
|
||||
|
@ -709,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// Apparently we're supposed to swallow this
|
||||
// SchedulesDirect returns 400 if no lineups are configured.
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
|
||||
{
|
||||
return false;
|
||||
|
|
|
@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
foreach (var programDto in currentProgramDtos)
|
||||
{
|
||||
if (currentChannelsDict.TryGetValue(programDto.ChannelId, out BaseItemDto channelDto))
|
||||
if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto))
|
||||
{
|
||||
channelDto.CurrentProgram = programDto;
|
||||
}
|
||||
|
@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
|
||||
|
||||
info.Name = program.Name;
|
||||
info.ChannelId = programDto.ChannelId;
|
||||
info.ChannelId = programDto.ChannelId ?? Guid.Empty;
|
||||
info.ChannelName = programDto.ChannelName;
|
||||
info.StartDate = program.StartDate;
|
||||
info.Name = program.Name;
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
internal class Channels
|
||||
{
|
||||
public string GuideNumber { get; set; }
|
||||
|
||||
public string GuideName { get; set; }
|
||||
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public string URL { get; set; }
|
||||
|
||||
public bool Favorite { get; set; }
|
||||
|
||||
public bool DRM { get; set; }
|
||||
|
||||
public bool HD { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
internal class DiscoverResponse
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
public string ModelNumber { get; set; }
|
||||
|
||||
public string FirmwareName { get; set; }
|
||||
|
||||
public string FirmwareVersion { get; set; }
|
||||
|
||||
public string DeviceID { get; set; }
|
||||
|
||||
public string DeviceAuth { get; set; }
|
||||
|
||||
public string BaseURL { get; set; }
|
||||
|
||||
public string LineupURL { get; set; }
|
||||
|
||||
public int TunerCount { get; set; }
|
||||
|
||||
public bool SupportsTranscoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var model = ModelNumber ?? string.Empty;
|
||||
|
||||
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,10 +8,12 @@ using System.Linq;
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
private readonly INetworkManager _networkManager;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||
|
||||
public HdHomerunHost(
|
||||
|
@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
_socketFactory = socketFactory;
|
||||
_networkManager = networkManager;
|
||||
_streamHelper = streamHelper;
|
||||
|
||||
_jsonOptions = JsonDefaults.GetOptions();
|
||||
}
|
||||
|
||||
public string Name => "HD Homerun";
|
||||
|
@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||
=> ChannelIdPrefix + i.GuideNumber;
|
||||
|
||||
private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
|
||||
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false) ?? new List<Channels>();
|
||||
|
||||
if (info.ImportFavoritesOnly)
|
||||
|
@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
Id = GetChannelId(info, i),
|
||||
IsFavorite = i.Favorite,
|
||||
TunerHostId = info.Id,
|
||||
IsHD = i.HD == 1,
|
||||
IsHD = i.HD,
|
||||
AudioCodec = i.AudioCodec,
|
||||
VideoCodec = i.VideoCodec,
|
||||
ChannelType = ChannelType.TV,
|
||||
|
@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}).Cast<ChannelInfo>().ToList();
|
||||
}
|
||||
|
||||
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
|
||||
internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = info.Id;
|
||||
|
||||
|
@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
try
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
|
@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
return new Uri(url).AbsoluteUri.TrimEnd('/');
|
||||
}
|
||||
|
||||
private class Channels
|
||||
{
|
||||
public string GuideNumber { get; set; }
|
||||
|
||||
public string GuideName { get; set; }
|
||||
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public string URL { get; set; }
|
||||
|
||||
public bool Favorite { get; set; }
|
||||
|
||||
public bool DRM { get; set; }
|
||||
|
||||
public int HD { get; set; }
|
||||
}
|
||||
|
||||
protected EncodingOptions GetEncodingOptions()
|
||||
{
|
||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
||||
|
@ -674,42 +662,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
}
|
||||
|
||||
public class DiscoverResponse
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
public string ModelNumber { get; set; }
|
||||
|
||||
public string FirmwareName { get; set; }
|
||||
|
||||
public string FirmwareVersion { get; set; }
|
||||
|
||||
public string DeviceID { get; set; }
|
||||
|
||||
public string DeviceAuth { get; set; }
|
||||
|
||||
public string BaseURL { get; set; }
|
||||
|
||||
public string LineupURL { get; set; }
|
||||
|
||||
public int TunerCount { get; set; }
|
||||
|
||||
public bool SupportsTranscoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var model = ModelNumber ?? string.Empty;
|
||||
|
||||
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_modelCache)
|
||||
|
@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
return list;
|
||||
}
|
||||
|
||||
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||
internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var hostInfo = new TunerHostInfo
|
||||
{
|
||||
|
@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
hostInfo.DeviceId = modelInfo.DeviceID;
|
||||
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
||||
hostInfo.TunerCount = modelInfo.TunerCount;
|
||||
|
||||
return hostInfo;
|
||||
}
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TasksChannelsCategory": "کانالهای داخلی",
|
||||
"TasksApplicationCategory": "برنامه",
|
||||
"TasksLibraryCategory": "کتابخانه",
|
||||
"TasksMaintenanceCategory": "تعمیر"
|
||||
"TasksMaintenanceCategory": "تعمیر",
|
||||
"Forced": "اجباری",
|
||||
"Default": "پیشفرض"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "Application : {0}, Appareil : {1}",
|
||||
"AppDeviceValues": "App : {0}, Appareil : {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
|
||||
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
|
||||
"Books": "Livres",
|
||||
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
|
||||
"Channels": "Chaînes",
|
||||
|
@ -11,12 +11,12 @@
|
|||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||
"DeviceOnlineWithName": "{0} est connecté",
|
||||
"FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
||||
"Favorites": "Favoris",
|
||||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artistes de l'album",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderContinueWatching": "Reprendre le visionnement",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes favoris",
|
||||
"HeaderFavoriteEpisodes": "Épisodes favoris",
|
||||
|
@ -26,12 +26,12 @@
|
|||
"HeaderNextUp": "À Suivre",
|
||||
"HeaderRecordingGroups": "Groupes d'enregistrements",
|
||||
"HomeVideos": "Vidéos personnelles",
|
||||
"Inherit": "Hériter",
|
||||
"Inherit": "Hérite",
|
||||
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
|
||||
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
|
||||
"LabelIpAddressValue": "Adresse IP : {0}",
|
||||
"LabelRunningTimeValue": "Durée : {0}",
|
||||
"Latest": "Derniers",
|
||||
"Latest": "Plus récent",
|
||||
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
|
||||
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
|
||||
|
@ -40,15 +40,15 @@
|
|||
"Movies": "Films",
|
||||
"Music": "Musique",
|
||||
"MusicVideos": "Vidéos musicales",
|
||||
"NameInstallFailed": "{0} échec d'installation",
|
||||
"NameInstallFailed": "échec d'installation de {0}",
|
||||
"NameSeasonNumber": "Saison {0}",
|
||||
"NameSeasonUnknown": "Saison Inconnue",
|
||||
"NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.",
|
||||
"NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
|
||||
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
|
||||
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
|
||||
"NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
|
||||
"NotificationOptionCameraImageUploaded": "Image d'appareil photo transférée",
|
||||
"NotificationOptionInstallationFailed": "Échec d'installation",
|
||||
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
|
||||
"NotificationOptionPluginError": "Erreur d'extension",
|
||||
|
@ -70,9 +70,9 @@
|
|||
"ScheduledTaskFailedWithName": "{0} a échoué",
|
||||
"ScheduledTaskStartedWithName": "{0} a commencé",
|
||||
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
|
||||
"Shows": "Émissions",
|
||||
"Shows": "Séries",
|
||||
"Songs": "Chansons",
|
||||
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
|
||||
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
||||
"Sync": "Synchroniser",
|
||||
|
@ -80,39 +80,43 @@
|
|||
"TvShows": "Séries Télé",
|
||||
"User": "Utilisateur",
|
||||
"UserCreatedWithName": "L'utilisateur {0} a été créé",
|
||||
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
|
||||
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
|
||||
"UserDeletedWithName": "L'utilisateur {0} supprimé",
|
||||
"UserDownloadingItemWithValues": "{0} télécharge {1}",
|
||||
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
|
||||
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
|
||||
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
|
||||
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
|
||||
"UserOfflineFromDevice": "{0} s'est déconnecté de {1}",
|
||||
"UserOnlineFromDevice": "{0} s'est connecté de {1}",
|
||||
"UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié",
|
||||
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
|
||||
"ValueSpecialEpisodeName": "Spécial - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TasksLibraryCategory": "Bibliothèque",
|
||||
"TasksLibraryCategory": "Médiathèque",
|
||||
"TasksMaintenanceCategory": "Entretien",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquant sur l'internet selon la configuration des métadonnées.",
|
||||
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
|
||||
"TaskRefreshChannels": "Rafraîchir des chaines",
|
||||
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines internet.",
|
||||
"TaskRefreshChannels": "Rafraîchir les chaines",
|
||||
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage datant de plus d'un jour.",
|
||||
"TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
|
||||
"TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.",
|
||||
"TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour les m.à.j. automatiques.",
|
||||
"TaskUpdatePlugins": "Mise à jour des extensions",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.",
|
||||
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
|
||||
"TaskRefreshPeople": "Rafraîchir les personnes",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux plus vieux que {0} jours.",
|
||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||
"TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshLibraryDescription": "Analyse votre médiathèque pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres.",
|
||||
"TaskRefreshLibrary": "Analyser la bibliothèque de médias",
|
||||
"TaskRefreshLibrary": "Analyser la médiathèque",
|
||||
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
|
||||
"TasksChannelsCategory": "Canaux Internet",
|
||||
"Default": "Par défaut"
|
||||
"TasksChannelsCategory": "Chaines Internet",
|
||||
"Default": "Par défaut",
|
||||
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
|
||||
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
|
||||
"Undefined": "Indéfini",
|
||||
"Forced": "Forcé"
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
|
||||
"LabelRunningTimeValue": "Garums: {0}",
|
||||
"Inherit": "Mantot",
|
||||
"AppDeviceValues": "Lietotne:{0}, Ierīce:{1}",
|
||||
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots tavai multvides bibliotēkai",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
|
||||
"UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
|
||||
|
@ -95,7 +95,7 @@
|
|||
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
|
||||
"TasksApplicationCategory": "Lietotne",
|
||||
"TasksLibraryCategory": "Bibliotēka",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus pēc metadatu uzstādījumiem.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
|
||||
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
|
||||
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
||||
"TaskRefreshChannels": "Atjaunot Kanālus",
|
||||
|
@ -103,14 +103,19 @@
|
|||
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
|
||||
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
|
||||
"TaskUpdatePlugins": "Atjaunot Paplašinājumus",
|
||||
"TaskRefreshPeopleDescription": "Atjauno metadatus priekš aktieriem un direktoriem tavā mediju bibliotēkā.",
|
||||
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
|
||||
"TaskRefreshPeople": "Atjaunot Cilvēkus",
|
||||
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
|
||||
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
|
||||
"TaskRefreshLibraryDescription": "Skenē tavas mediju bibliotēkas priekš jaunām datnēm un atjauno metadatus.",
|
||||
"TaskRefreshLibrary": "Skanēt Mediju Bibliotēku",
|
||||
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
|
||||
"TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
|
||||
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
|
||||
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
|
||||
"TasksChannelsCategory": "Interneta Kanāli",
|
||||
"TasksMaintenanceCategory": "Apkope"
|
||||
"TasksMaintenanceCategory": "Apkope",
|
||||
"Forced": "Piespiests",
|
||||
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
|
||||
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
|
||||
"Undefined": "Nenoteikts",
|
||||
"Default": "Noklusējums"
|
||||
}
|
||||
|
|
|
@ -113,5 +113,10 @@
|
|||
"TasksApplicationCategory": "Aplikacija",
|
||||
"TasksLibraryCategory": "Knjižnica",
|
||||
"TasksMaintenanceCategory": "Vzdrževanje",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu."
|
||||
"TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu.",
|
||||
"TaskCleanActivityLogDescription": "Počisti zapise v dnevniku aktivnosti starejše od nastavljenega časa.",
|
||||
"TaskCleanActivityLog": "Počisti dnevnik aktivnosti",
|
||||
"Undefined": "Nedoločen",
|
||||
"Forced": "Prisilno",
|
||||
"Default": "Privzeto"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
|
@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
|
|||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
|
|
|
@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
|
|||
/// <inheritdoc />
|
||||
public event EventHandler<SessionEventArgs> SessionActivity;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all connections.
|
||||
/// </summary>
|
||||
|
@ -312,6 +315,19 @@ namespace Emby.Server.Implementations.Session
|
|||
return session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnSessionControllerConnected(SessionInfo info)
|
||||
{
|
||||
EventHelper.QueueEventIfNotNull(
|
||||
SessionControllerConnected,
|
||||
this,
|
||||
new SessionEventArgs
|
||||
{
|
||||
SessionInfo = info
|
||||
},
|
||||
_logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CloseIfNeeded(SessionInfo session)
|
||||
{
|
||||
|
|
|
@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
|
|||
|
||||
var controller = (WebSocketController)controllerInfo.Item1;
|
||||
controller.AddWebSocket(connection);
|
||||
|
||||
_sessionManager.OnSessionControllerConnected(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The map between users and counter of active sessions.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<Guid, int> _activeUsers =
|
||||
new ConcurrentDictionary<Guid, int>();
|
||||
|
||||
/// <summary>
|
||||
/// The map between sessions and groups.
|
||||
/// </summary>
|
||||
|
@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
||||
_sessionManager.SessionStarted += OnSessionManagerSessionStarted;
|
||||
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
throw new InvalidOperationException("Could not add session to group!");
|
||||
}
|
||||
|
||||
UpdateSessionsCounter(session.UserId, 1);
|
||||
group.CreateGroup(session, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
if (existingGroup.GroupId.Equals(request.GroupId))
|
||||
{
|
||||
// Restore session.
|
||||
UpdateSessionsCounter(session.UserId, 1);
|
||||
group.SessionJoin(session, request, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
throw new InvalidOperationException("Could not add session to group!");
|
||||
}
|
||||
|
||||
UpdateSessionsCounter(session.UserId, 1);
|
||||
group.SessionJoin(session, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
throw new InvalidOperationException("Could not remove session from group!");
|
||||
}
|
||||
|
||||
UpdateSessionsCounter(session.UserId, -1);
|
||||
group.SessionLeave(session, request, cancellationToken);
|
||||
|
||||
if (group.IsGroupEmpty())
|
||||
|
@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsUserActive(Guid userId)
|
||||
{
|
||||
if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
|
||||
{
|
||||
return sessionsCounter > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
|
@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
return;
|
||||
}
|
||||
|
||||
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
||||
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
|
||||
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
|
@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
JoinGroup(session, request, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSessionsCounter(Guid userId, int toAdd)
|
||||
{
|
||||
// Update sessions counter.
|
||||
var newSessionsCounter = _activeUsers.AddOrUpdate(
|
||||
userId,
|
||||
1,
|
||||
(key, sessionsCounter) => sessionsCounter + toAdd);
|
||||
|
||||
// Should never happen.
|
||||
if (newSessionsCounter < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Sessions counter is negative!");
|
||||
}
|
||||
|
||||
// Clean record if user has no more active sessions.
|
||||
if (newSessionsCounter == 0)
|
||||
{
|
||||
_activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
}
|
||||
|
||||
// Don't add a package that doesn't have any compatible versions.
|
||||
if (package.Versions.Count == 0)
|
||||
if (package.versions.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -555,6 +555,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
||||
|
|
|
@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
|
|||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.SyncPlay;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||
/// </summary>
|
||||
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
|
||||
{
|
||||
private readonly ISyncPlayManager _syncPlayManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
|
||||
/// </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="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public SyncPlayAccessHandler(
|
||||
ISyncPlayManager syncPlayManager,
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_syncPlayManager = syncPlayManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
|
@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||
var userId = ClaimHelpers.GetUserId(context.User);
|
||||
var user = _userManager.GetUserById(userId!.Value);
|
||||
|
||||
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess)
|
||||
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
|
||||
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|
||||
{
|
||||
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
|
||||
{
|
||||
|
|
|
@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param>
|
||||
public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess)
|
||||
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
|
||||
public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
|
||||
{
|
||||
RequiredAccess = requiredAccess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
||||
/// </summary>
|
||||
public SyncPlayAccessRequirement()
|
||||
{
|
||||
RequiredAccess = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the required SyncPlay access.
|
||||
/// </summary>
|
||||
public SyncPlayAccess? RequiredAccess { get; }
|
||||
public SyncPlayAccessRequirementType RequiredAccess { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
|
|||
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring access to SyncPlay.
|
||||
/// Policy name for accessing SyncPlay.
|
||||
/// </summary>
|
||||
public const string SyncPlayAccess = "SyncPlayAccess";
|
||||
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring group creation access to SyncPlay.
|
||||
/// Policy name for creating a SyncPlay group.
|
||||
/// </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.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
|
@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers
|
|||
public class DisplayPreferencesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
||||
private readonly ILogger<DisplayPreferencesController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
Client = displayPreferences.Client,
|
||||
Id = displayPreferences.ItemId.ToString(),
|
||||
ViewType = itemPreferences.ViewType.ToString(),
|
||||
SortBy = itemPreferences.SortBy,
|
||||
SortOrder = itemPreferences.SortOrder,
|
||||
IndexBy = displayPreferences.IndexBy?.ToString(),
|
||||
|
@ -77,11 +80,6 @@ namespace Jellyfin.Api.Controllers
|
|||
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["skipForwardLength"] = displayPreferences.SkipForwardLength.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)))
|
||||
{
|
||||
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);
|
||||
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
|
||||
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
|
||||
displayPreferences.CustomPrefs.Remove(key);
|
||||
}
|
||||
}
|
||||
|
@ -204,11 +201,6 @@ namespace Jellyfin.Api.Controllers
|
|||
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
||||
itemPrefs.ItemId = itemId;
|
||||
|
||||
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
|
||||
{
|
||||
itemPrefs.ViewType = viewType;
|
||||
}
|
||||
|
||||
// Set all remaining custom preferences.
|
||||
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
||||
_displayPreferencesManager.SaveChanges();
|
||||
|
|
|
@ -98,7 +98,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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);
|
||||
|
@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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);
|
||||
|
@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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);
|
||||
|
@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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);
|
||||
|
|
|
@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
|
@ -119,7 +120,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] bool? enableTranscoding,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromBody] PlaybackInfoDto? playbackInfoDto)
|
||||
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
|
||||
{
|
||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -17,6 +18,7 @@ using MediaBrowser.Model.Querying;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
|
@ -53,6 +55,13 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Creates a new playlist.
|
||||
/// </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>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
|
||||
|
@ -61,14 +70,23 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
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
|
||||
{
|
||||
Name = createPlaylistRequest.Name,
|
||||
ItemIdList = createPlaylistRequest.Ids,
|
||||
UserId = createPlaylistRequest.UserId,
|
||||
MediaType = createPlaylistRequest.MediaType
|
||||
Name = name ?? createPlaylistRequest?.Name,
|
||||
ItemIdList = ids,
|
||||
UserId = userId ?? createPlaylistRequest?.UserId ?? default,
|
||||
MediaType = mediaType ?? createPlaylistRequest?.MediaType
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
|
|
|
@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
if (_quickConnect.State == QuickConnectState.Unavailable)
|
||||
{
|
||||
return Forbid("Quick connect is unavailable");
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
|
||||
}
|
||||
|
||||
_quickConnect.Activate();
|
||||
|
@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
|
|||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return Forbid("Unknown user id");
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
|
||||
}
|
||||
|
||||
return _quickConnect.AuthorizeRequest(userId.Value, code);
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// The sync play controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
||||
[Authorize(Policy = Policies.SyncPlayHasAccess)]
|
||||
public class SyncPlayController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("New")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
|
||||
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
|
||||
public ActionResult SyncPlayCreateGroup(
|
||||
[FromBody, Required] NewGroupRequestDto requestData)
|
||||
{
|
||||
|
@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Join")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
||||
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||
public ActionResult SyncPlayJoinGroup(
|
||||
[FromBody, Required] JoinGroupRequestDto requestData)
|
||||
{
|
||||
|
@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Leave")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayLeaveGroup()
|
||||
{
|
||||
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>
|
||||
[HttpGet("List")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
||||
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
|
@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("SetNewQueue")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetNewQueue(
|
||||
[FromBody, Required] PlayRequestDto requestData)
|
||||
{
|
||||
|
@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("SetPlaylistItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetPlaylistItem(
|
||||
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
||||
{
|
||||
|
@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("RemoveFromPlaylist")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayRemoveFromPlaylist(
|
||||
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
||||
{
|
||||
|
@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("MovePlaylistItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayMovePlaylistItem(
|
||||
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
||||
{
|
||||
|
@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Queue")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayQueue(
|
||||
[FromBody, Required] QueueRequestDto requestData)
|
||||
{
|
||||
|
@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Unpause")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayUnpause()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
|
@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Pause")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayPause()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
|
@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Stop")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayStop()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
|
@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Seek")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySeek(
|
||||
[FromBody, Required] SeekRequestDto requestData)
|
||||
{
|
||||
|
@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Buffering")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayBuffering(
|
||||
[FromBody, Required] BufferRequestDto requestData)
|
||||
{
|
||||
|
@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Ready")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayReady(
|
||||
[FromBody, Required] ReadyRequestDto requestData)
|
||||
{
|
||||
|
@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("SetIgnoreWait")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetIgnoreWait(
|
||||
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
||||
{
|
||||
|
@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("NextItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayNextItem(
|
||||
[FromBody, Required] NextItemRequestDto requestData)
|
||||
{
|
||||
|
@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("PreviousItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayPreviousItem(
|
||||
[FromBody, Required] PreviousItemRequestDto requestData)
|
||||
{
|
||||
|
@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("SetRepeatMode")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetRepeatMode(
|
||||
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
||||
{
|
||||
|
@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("SetShuffleMode")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetShuffleMode(
|
||||
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
||||
{
|
||||
|
|
|
@ -133,11 +133,11 @@ namespace Jellyfin.Api.Controllers
|
|||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DeleteUser([FromRoute, Required] Guid userId)
|
||||
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
_sessionManager.RevokeUserTokens(user.Id, null);
|
||||
_userManager.DeleteUser(userId);
|
||||
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
|
|||
|
||||
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
|
||||
|
@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> UpdateUserPassword(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UpdateUserPassword request)
|
||||
[FromBody, Required] UpdateUserPassword request)
|
||||
{
|
||||
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);
|
||||
|
@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
|
|||
|
||||
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);
|
||||
|
@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateUserEasyPassword(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UpdateUserEasyPassword request)
|
||||
[FromBody, Required] UpdateUserEasyPassword request)
|
||||
{
|
||||
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);
|
||||
|
@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> UpdateUser(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UserDto updateUser)
|
||||
[FromBody, Required] UserDto updateUser)
|
||||
{
|
||||
if (updateUser == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> UpdateUserPolicy(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UserPolicy newPolicy)
|
||||
[FromBody, Required] UserPolicy newPolicy)
|
||||
{
|
||||
if (newPolicy == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
// If removing admin access
|
||||
|
@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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 (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
return Forbid("Administrators cannot be disabled.");
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
|
||||
}
|
||||
|
||||
// If disabling
|
||||
|
@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
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;
|
||||
|
@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> UpdateUserConfiguration(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UserConfiguration userConfig)
|
||||
[FromBody, Required] UserConfiguration userConfig)
|
||||
{
|
||||
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);
|
||||
|
@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpPost("New")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[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);
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Merges videos into a single record.
|
||||
/// </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="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>
|
||||
|
@ -204,9 +204,9 @@ namespace Jellyfin.Api.Controllers
|
|||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[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))
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.Id)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<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.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Jellyfin.Api.Models.PlaylistDtos
|
|||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media type.
|
||||
|
|
|
@ -18,7 +18,8 @@ namespace Jellyfin.Data.Entities
|
|||
/// <param name="name">The name.</param>
|
||||
/// <param name="type">The type.</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))
|
||||
{
|
||||
|
@ -34,7 +35,7 @@ namespace Jellyfin.Data.Entities
|
|||
Type = type;
|
||||
UserId = userId;
|
||||
DateCreated = DateTime.UtcNow;
|
||||
LogSeverity = LogLevel.Trace;
|
||||
LogSeverity = logLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -23,7 +23,6 @@ namespace Jellyfin.Data.Entities
|
|||
Client = client;
|
||||
|
||||
SortBy = "SortName";
|
||||
ViewType = ViewType.Poster;
|
||||
SortOrder = SortOrder.Ascending;
|
||||
RememberSorting = false;
|
||||
RememberIndexing = false;
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
|
|||
EnableAutoLogin = false;
|
||||
PlayDefaultAudioTrack = true;
|
||||
SubtitleMode = SubtitlePlaybackMode.Default;
|
||||
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
|
||||
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
|
||||
|
||||
AddDefaultPermissions();
|
||||
AddDefaultPreferences();
|
||||
|
@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
|
|||
/// <summary>
|
||||
/// Gets or sets the level of sync play permissions this user has.
|
||||
/// </summary>
|
||||
public SyncPlayAccess SyncPlayAccess { get; set; }
|
||||
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum SyncPlayAccess.
|
||||
/// Enum SyncPlayUserAccessType.
|
||||
/// </summary>
|
||||
public enum SyncPlayAccess
|
||||
public enum SyncPlayUserAccessType
|
||||
{
|
||||
/// <summary>
|
||||
/// User can create groups and join them.
|
|
@ -1,4 +1,4 @@
|
|||
namespace Jellyfin.Data.Enums
|
||||
namespace Jellyfin.Data.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// An enum representing the type of view for a library or collection.
|
||||
|
@ -6,33 +6,108 @@
|
|||
public enum ViewType
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows banners.
|
||||
/// Shows albums.
|
||||
/// </summary>
|
||||
Banner = 0,
|
||||
Albums = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Shows a list of content.
|
||||
/// Shows album artists.
|
||||
/// </summary>
|
||||
List = 1,
|
||||
AlbumArtists = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Shows poster artwork.
|
||||
/// Shows artists.
|
||||
/// </summary>
|
||||
Poster = 2,
|
||||
Artists = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Shows poster artwork with a card containing the name and year.
|
||||
/// Shows channels.
|
||||
/// </summary>
|
||||
PosterCard = 3,
|
||||
Channels = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Shows a thumbnail.
|
||||
/// Shows collections.
|
||||
/// </summary>
|
||||
Thumb = 4,
|
||||
Collections = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Shows a thumbnail with a card containing the name and year.
|
||||
/// Shows episodes.
|
||||
/// </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>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -435,7 +435,7 @@ namespace Jellyfin.Drawing.Skia
|
|||
0f,
|
||||
kernelOffset,
|
||||
SKShaderTileMode.Clamp,
|
||||
false);
|
||||
true);
|
||||
|
||||
canvas.DrawBitmap(
|
||||
source,
|
||||
|
|
|
@ -27,6 +27,16 @@ namespace Jellyfin.Networking.Configuration
|
|||
/// </summary>
|
||||
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>
|
||||
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
|
||||
/// </summary>
|
||||
|
@ -83,7 +93,7 @@ namespace Jellyfin.Networking.Configuration
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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>
|
||||
public bool EnableHttps { get; set; }
|
||||
|
||||
|
|
|
@ -1314,9 +1314,7 @@ namespace Jellyfin.Networking.Manager
|
|||
return true;
|
||||
}
|
||||
|
||||
// Have to return something, so return an internal address
|
||||
|
||||
_logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source);
|
||||
_logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Activity
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
|
||||
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task CreateAsync(ActivityLog entry)
|
||||
|
|
|
@ -86,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
|
|||
return name;
|
||||
}
|
||||
|
||||
private static string GetPlaybackNotificationType(string mediaType)
|
||||
private static string? GetPlaybackNotificationType(string mediaType)
|
||||
{
|
||||
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
|
|
@ -94,7 +94,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
|
|||
return name;
|
||||
}
|
||||
|
||||
private static string GetPlaybackStoppedNotificationType(string mediaType)
|
||||
private static string? GetPlaybackStoppedNotificationType(string mediaType)
|
||||
{
|
||||
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
|
@ -25,11 +26,11 @@
|
|||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable enable
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CA1307
|
||||
#pragma warning disable CA1307
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
@ -220,7 +219,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DeleteUser(Guid userId)
|
||||
public async Task DeleteUserAsync(Guid userId)
|
||||
{
|
||||
if (!_users.TryGetValue(userId, out var user))
|
||||
{
|
||||
|
@ -246,7 +245,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
nameof(userId));
|
||||
}
|
||||
|
||||
using var dbContext = _dbProvider.CreateContext();
|
||||
await using var dbContext = _dbProvider.CreateContext();
|
||||
|
||||
// Clear all entities related to the user from the database.
|
||||
if (user.ProfileImage != null)
|
||||
|
@ -258,10 +257,10 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
dbContext.RemoveRange(user.Preferences);
|
||||
dbContext.RemoveRange(user.AccessSchedules);
|
||||
dbContext.Users.Remove(user);
|
||||
dbContext.SaveChanges();
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_users.Remove(userId);
|
||||
|
||||
_eventManager.Publish(new UserDeletedEventArgs(user));
|
||||
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -13,9 +13,9 @@ namespace Jellyfin.Server.Implementations.ValueConverters
|
|||
/// </summary>
|
||||
/// <param name="kind">The kind to specify.</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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,5 +107,28 @@ namespace Jellyfin.Server.Extensions
|
|||
{
|
||||
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.Formatters;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -127,18 +128,32 @@ namespace Jellyfin.Server.Extensions
|
|||
policy.AddRequirements(new RequiresElevationRequirement());
|
||||
});
|
||||
options.AddPolicy(
|
||||
Policies.SyncPlayAccess,
|
||||
Policies.SyncPlayHasAccess,
|
||||
policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups));
|
||||
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
|
||||
});
|
||||
options.AddPolicy(
|
||||
Policies.SyncPlayCreateGroupAccess,
|
||||
Policies.SyncPlayCreateGroup,
|
||||
policy =>
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
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
|
||||
{
|
||||
Type = "file"
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
<PackageReference Include="CommandLineParser" Version="2.8.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.Diagnostics.HealthChecks" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.1" />
|
||||
<PackageReference Include="prometheus-net" Version="4.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.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 }
|
||||
};
|
||||
|
||||
var customDisplayPrefs = new HashSet<string>();
|
||||
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
|
|
@ -128,6 +128,8 @@ namespace Jellyfin.Server
|
|||
mainApp.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
// This must be injected before any path related middleware.
|
||||
mainApp.UsePathTrim();
|
||||
mainApp.UseStaticFiles();
|
||||
if (appConfig.HostWebClient())
|
||||
{
|
||||
|
@ -142,6 +144,8 @@ namespace Jellyfin.Server
|
|||
RequestPath = "/web",
|
||||
ContentTypeProvider = extensionProvider
|
||||
});
|
||||
|
||||
mainApp.UseRobotsRedirection();
|
||||
}
|
||||
|
||||
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.Globalization;
|
||||
using System.Text.Json;
|
||||
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)
|
||||
{
|
||||
var guidStr = reader.GetString();
|
||||
|
||||
return guidStr == null ? Guid.Empty : new Guid(guidStr);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == Guid.Empty)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ namespace MediaBrowser.Common.Json
|
|||
options.Converters.Add(new JsonVersionConverter());
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
options.Converters.Add(new JsonNullableStructConverterFactory());
|
||||
options.Converters.Add(new JsonBoolNumberConverter());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -48,10 +48,10 @@ namespace MediaBrowser.Controller.BaseItemManager
|
|||
return !baseItem.EnableMediaSourceDisplay;
|
||||
}
|
||||
|
||||
var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
|
||||
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
|
||||
if (typeOptions != null)
|
||||
{
|
||||
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.BaseItemManager
|
|||
return !baseItem.EnableMediaSourceDisplay;
|
||||
}
|
||||
|
||||
var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
|
||||
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
|
||||
if (typeOptions != null)
|
||||
{
|
||||
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
|
|
|
@ -1385,6 +1385,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
new List<FileSystemMetadata>();
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -354,6 +354,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -571,7 +571,7 @@ namespace MediaBrowser.Controller.Library
|
|||
string videoPath,
|
||||
string[] files);
|
||||
|
||||
void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason);
|
||||
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
|
||||
|
||||
BaseItem GetParentItem(string parentId, Guid? userId);
|
||||
|
||||
|
|
|
@ -93,7 +93,8 @@ namespace MediaBrowser.Controller.Library
|
|||
/// Deletes the specified user.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Resets the password.
|
||||
|
|
|
@ -46,6 +46,11 @@ namespace MediaBrowser.Controller.Session
|
|||
|
||||
event EventHandler<SessionEventArgs> SessionActivity;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [session controller connected].
|
||||
/// </summary>
|
||||
event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [capabilities changed].
|
||||
/// </summary>
|
||||
|
@ -78,6 +83,12 @@ namespace MediaBrowser.Controller.Session
|
|||
/// <param name="user">The user.</param>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -51,5 +51,12 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
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.
|
||||
// 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);
|
||||
if (enableThumbnail)
|
||||
{
|
||||
var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
|
||||
var batchSize = useLargerBatchSize ? "50" : "24";
|
||||
if (string.IsNullOrEmpty(vf))
|
||||
{
|
||||
vf = "-vf thumbnail=24";
|
||||
vf = "-vf thumbnail=" + batchSize;
|
||||
}
|
||||
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.
|
||||
OpenclDevice = "0.0";
|
||||
EnableTonemapping = false;
|
||||
TonemappingAlgorithm = "reinhard";
|
||||
TonemappingAlgorithm = "hable";
|
||||
TonemappingRange = "auto";
|
||||
TonemappingDesat = 0;
|
||||
TonemappingThreshold = 0.8;
|
||||
TonemappingPeak = 0;
|
||||
TonemappingPeak = 100;
|
||||
TonemappingParam = 0;
|
||||
H264Crf = 23;
|
||||
H265Crf = 28;
|
||||
|
|
|
@ -152,7 +152,7 @@ namespace MediaBrowser.Model.Dto
|
|||
/// Gets or sets the channel identifier.
|
||||
/// </summary>
|
||||
/// <value>The channel identifier.</value>
|
||||
public Guid ChannelId { get; set; }
|
||||
public Guid? ChannelId { get; set; }
|
||||
|
||||
public string ChannelName { get; set; }
|
||||
|
||||
|
@ -270,7 +270,7 @@ namespace MediaBrowser.Model.Dto
|
|||
/// Gets or sets the parent id.
|
||||
/// </summary>
|
||||
/// <value>The parent id.</value>
|
||||
public Guid ParentId { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
|
@ -344,13 +344,13 @@ namespace MediaBrowser.Model.Dto
|
|||
/// Gets or sets the series id.
|
||||
/// </summary>
|
||||
/// <value>The series id.</value>
|
||||
public Guid SeriesId { get; set; }
|
||||
public Guid? SeriesId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the season identifier.
|
||||
/// </summary>
|
||||
/// <value>The season identifier.</value>
|
||||
public Guid SeasonId { get; set; }
|
||||
public Guid? SeasonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the special feature count.
|
||||
|
@ -428,7 +428,7 @@ namespace MediaBrowser.Model.Dto
|
|||
/// Gets or sets the album id.
|
||||
/// </summary>
|
||||
/// <value>The album id.</value>
|
||||
public Guid AlbumId { get; set; }
|
||||
public Guid? AlbumId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>Access level to SyncPlay features.</value>
|
||||
public SyncPlayAccess SyncPlayAccess { get; set; }
|
||||
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
|
||||
|
||||
public UserPolicy()
|
||||
{
|
||||
|
@ -160,7 +160,7 @@ namespace MediaBrowser.Model.Users
|
|||
EnableContentDownloading = true;
|
||||
EnablePublicSharing = 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);
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
|
@ -239,6 +239,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
||||
{
|
||||
var itemUpdateType = ItemUpdateType.MetadataDownload;
|
||||
var saveEntity = false;
|
||||
var personEntity = LibraryManager.GetPerson(person.Name);
|
||||
foreach (var id in person.ProviderIds)
|
||||
|
@ -261,18 +262,18 @@ namespace MediaBrowser.Providers.Manager
|
|||
0);
|
||||
|
||||
saveEntity = true;
|
||||
itemUpdateType = ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
||||
if (saveEntity)
|
||||
{
|
||||
personsToSave.Add(personEntity);
|
||||
await LibraryManager.RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload);
|
||||
LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
|
|
|
@ -425,7 +425,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||
{
|
||||
var person = new PersonInfo
|
||||
{
|
||||
Name = result.Director.Trim(),
|
||||
Name = result.Writer.Trim(),
|
||||
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)
|
||||
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
|
||||
|
||||
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
|
||||
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.
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -15,7 +15,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,7 +16,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -15,7 +15,7 @@ RUN apt-get update \
|
|||
|
||||
# Install dotnet repository
|
||||
# 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 \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
<PackageReference Include="AutoFixture" Version="4.14.0" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" 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.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.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<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.runner.visualstudio" Version="2.4.3" />
|
||||
<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.Globalization;
|
||||
using System.Text.Json;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Common.Tests.Extensions
|
||||
namespace Jellyfin.Common.Tests.Json
|
||||
{
|
||||
public class JsonGuidConverterTests
|
||||
{
|
||||
|
@ -44,9 +45,25 @@ namespace Jellyfin.Common.Tests.Extensions
|
|||
}
|
||||
|
||||
[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>
|
||||
|
||||
<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.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<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.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0" />
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</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.runner.visualstudio" Version="2.4.3" />
|
||||
<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…
Reference in New Issue
Block a user