Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24d482b36b | |||
| fff4477a93 | |||
| 9810d22d96 | |||
| 5027e3cd53 | |||
| 9645955629 | |||
| 078ee1f2de | |||
| 236c7649dd | |||
| c1e52df0b7 | |||
| be949af59e | |||
| 122da8f447 | |||
| 486c7fa51e | |||
| 6709c80f0a | |||
| 3f3145600c | |||
| bc613b8344 | |||
| 0eb5897100 | |||
| ee0094d889 | |||
| 7051a18be0 | |||
| fce3a5d241 | |||
| 900acc03aa | |||
| a475a7d50a | |||
| b7bc0e1c96 | |||
| 1739962f52 | |||
| 7f0f93eb4a | |||
| 71c13057f4 | |||
| 7f12677dc3 | |||
| 4f2b1736ab | |||
| c05049e54e | |||
| dd5f6406a2 | |||
| 79c0a7d7f0 | |||
| 3c6485f0a1 | |||
| 8a8b2c4380 | |||
| 7403428864 | |||
| 235da65a75 | |||
| 26eab7aa2e | |||
| d235378133 | |||
| 5a62c7a146 |
@@ -16,13 +16,12 @@
|
|||||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
|
|
||||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.5" />
|
<PackageVersion Include="libse" Version="4.0.7" />
|
||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
|
||||||
@@ -59,12 +58,12 @@
|
|||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.9.8</VersionPrefix>
|
<VersionPrefix>10.9.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations
|
|||||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||||
{ BindToUnixSocketKey, bool.FalseString },
|
{ BindToUnixSocketKey, bool.FalseString },
|
||||||
{ SqliteCacheSizeKey, "20000" },
|
{ SqliteCacheSizeKey, "20000" }
|
||||||
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||||
|
|
||||||
var activityDate = DateTime.UtcNow;
|
var activityDate = DateTime.UtcNow;
|
||||||
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
||||||
var lastActivityDate = session.LastActivityDate;
|
var lastActivityDate = session.LastActivityDate;
|
||||||
session.LastActivityDate = activityDate;
|
session.LastActivityDate = activityDate;
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <returns>SessionInfo.</returns>
|
/// <returns>SessionInfo.</returns>
|
||||||
private async Task<SessionInfo> GetSessionInfo(
|
private SessionInfo GetSessionInfo(
|
||||||
string appName,
|
string appName,
|
||||||
string appVersion,
|
string appVersion,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
||||||
{
|
{
|
||||||
sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
||||||
_activeConnections[key] = sessionInfo;
|
_activeConnections[key] = sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return sessionInfo;
|
return sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SessionInfo> CreateSession(
|
private SessionInfo CreateSession(
|
||||||
string key,
|
string key,
|
||||||
string appName,
|
string appName,
|
||||||
string appVersion,
|
string appVersion,
|
||||||
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
deviceName = "Network Device";
|
deviceName = "Network Device";
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
|
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
|
||||||
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
||||||
{
|
{
|
||||||
sessionInfo.DeviceName = deviceName;
|
sessionInfo.DeviceName = deviceName;
|
||||||
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return new[] { item };
|
return new[] { item };
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(id);
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
|
||||||
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return new List<BaseItem>();
|
return new List<BaseItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
|
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1520,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
|
|||||||
// This should be validated above, but if it isn't don't delete all tokens.
|
// This should be validated above, but if it isn't don't delete all tokens.
|
||||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||||
|
|
||||||
var existing = (await _deviceManager.GetDevices(
|
var existing = _deviceManager.GetDevices(
|
||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
UserId = user.Id
|
UserId = user.Id
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
foreach (var auth in existing)
|
foreach (var auth in existing)
|
||||||
{
|
{
|
||||||
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
||||||
|
|
||||||
var existing = (await _deviceManager.GetDevices(
|
var existing = _deviceManager.GetDevices(
|
||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
AccessToken = accessToken
|
AccessToken = accessToken
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
if (existing.Count > 0)
|
if (existing.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
|
|||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var existing = await _deviceManager.GetDevices(new DeviceQuery
|
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
UserId = userId
|
UserId = userId
|
||||||
}).ConfigureAwait(false);
|
});
|
||||||
|
|
||||||
foreach (var info in existing.Items)
|
foreach (var info in existing.Items)
|
||||||
{
|
{
|
||||||
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
||||||
{
|
{
|
||||||
var items = (await _deviceManager.GetDevices(new DeviceQuery
|
var items = _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
AccessToken = token,
|
AccessToken = token,
|
||||||
Limit = 1
|
Limit = 1
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
if (items.Count == 0)
|
if (items.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
string? presentationUniqueKey = null;
|
string? presentationUniqueKey = null;
|
||||||
int? limit = request.Limit;
|
int? limit = null;
|
||||||
if (!request.SeriesId.IsNullOrEmpty())
|
if (!request.SeriesId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
|
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
|
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
|
return _deviceManager.GetDevicesForUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
[HttpGet("Info")]
|
[HttpGet("Info")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
var deviceInfo = _deviceManager.GetDevice(id);
|
||||||
if (deviceInfo is null)
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
[HttpGet("Options")]
|
[HttpGet("Options")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
||||||
if (deviceInfo is null)
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
var existingDevice = _deviceManager.GetDevice(id);
|
||||||
if (existingDevice is null)
|
if (existingDevice is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
|
||||||
|
|
||||||
foreach (var session in sessions.Items)
|
foreach (var session in sessions.Items)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2089,6 +2089,8 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||||
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
||||||
|
|
||||||
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
|
|
||||||
if (disableCaching)
|
if (disableCaching)
|
||||||
{
|
{
|
||||||
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
|
|
||||||
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
||||||
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
|
|||||||
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
||||||
if (System.IO.File.Exists(path))
|
if (System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.9.8</VersionPrefix>
|
<VersionPrefix>10.9.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
||||||
|
private readonly ConcurrentDictionary<int, Device> _devices;
|
||||||
|
private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
||||||
@@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
{
|
{
|
||||||
_dbProvider = dbProvider;
|
_dbProvider = dbProvider;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_devices = new ConcurrentDictionary<int, Device>();
|
||||||
|
_deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
|
||||||
|
|
||||||
|
using var dbContext = _dbProvider.CreateDbContext();
|
||||||
|
foreach (var device in dbContext.Devices
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
|
.AsEnumerable())
|
||||||
|
{
|
||||||
|
_devices.TryAdd(device.Id, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var deviceOption in dbContext.DeviceOptions
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
|
.AsEnumerable())
|
||||||
|
{
|
||||||
|
_deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_deviceOptions[deviceId] = deviceOptions;
|
||||||
|
|
||||||
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,25 +97,17 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
dbContext.Devices.Add(device);
|
dbContext.Devices.Add(device);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
_devices.TryAdd(device.Id, device);
|
||||||
}
|
}
|
||||||
|
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
|
public DeviceOptions GetDeviceOptions(string deviceId)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
_deviceOptions.TryGetValue(deviceId, out var deviceOptions);
|
||||||
DeviceOptions? deviceOptions;
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
deviceOptions = await dbContext.DeviceOptions
|
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(d => d.DeviceId == deviceId)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceOptions ?? new DeviceOptions(deviceId);
|
return deviceOptions ?? new DeviceOptions(deviceId);
|
||||||
}
|
}
|
||||||
@@ -108,57 +121,43 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DeviceInfo?> GetDevice(string id)
|
public DeviceInfo? GetDevice(string id)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
|
||||||
await using (dbContext.ConfigureAwait(false))
|
_deviceOptions.TryGetValue(id, out var deviceOption);
|
||||||
{
|
|
||||||
var device = await dbContext.Devices
|
|
||||||
.Where(d => d.DeviceId == id)
|
|
||||||
.OrderByDescending(d => d.DateLastActivity)
|
|
||||||
.Include(d => d.User)
|
|
||||||
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
|
var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
|
||||||
|
return deviceInfo;
|
||||||
return deviceInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
|
public QueryResult<Device> GetDevices(DeviceQuery query)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
IEnumerable<Device> devices = _devices.Values
|
||||||
await using (dbContext.ConfigureAwait(false))
|
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
|
||||||
|
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
|
||||||
|
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
|
||||||
|
.OrderBy(d => d.Id)
|
||||||
|
.ToList();
|
||||||
|
var count = devices.Count();
|
||||||
|
|
||||||
|
if (query.Skip.HasValue)
|
||||||
{
|
{
|
||||||
var devices = dbContext.Devices
|
devices = devices.Skip(query.Skip.Value);
|
||||||
.OrderBy(d => d.Id)
|
|
||||||
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
|
|
||||||
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
|
|
||||||
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
|
|
||||||
|
|
||||||
var count = await devices.CountAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (query.Skip.HasValue)
|
|
||||||
{
|
|
||||||
devices = devices.Skip(query.Skip.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
|
||||||
{
|
|
||||||
devices = devices.Take(query.Limit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.Limit.HasValue)
|
||||||
|
{
|
||||||
|
devices = devices.Take(query.Limit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QueryResult<Device>(query.Skip, count, devices.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
|
public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
|
||||||
{
|
{
|
||||||
var devices = await GetDevices(query).ConfigureAwait(false);
|
var devices = GetDevices(query);
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(
|
return new QueryResult<DeviceInfo>(
|
||||||
devices.StartIndex,
|
devices.StartIndex,
|
||||||
@@ -167,38 +166,36 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
|
public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
IEnumerable<Device> devices = _devices.Values
|
||||||
await using (dbContext.ConfigureAwait(false))
|
.OrderByDescending(d => d.DateLastActivity)
|
||||||
|
.ThenBy(d => d.DeviceId);
|
||||||
|
|
||||||
|
if (!userId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
var sessions = dbContext.Devices
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
.Include(d => d.User)
|
if (user is null)
|
||||||
.OrderByDescending(d => d.DateLastActivity)
|
|
||||||
.ThenBy(d => d.DeviceId)
|
|
||||||
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
|
||||||
.AsAsyncEnumerable();
|
|
||||||
|
|
||||||
if (!userId.IsNullOrEmpty())
|
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId.Value);
|
throw new ResourceNotFoundException();
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
|
devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(array);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var array = devices.Select(device =>
|
||||||
|
{
|
||||||
|
_deviceOptions.TryGetValue(device.DeviceId, out var option);
|
||||||
|
return ToDeviceInfo(device, option);
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
return new QueryResult<DeviceInfo>(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteDevice(Device device)
|
public async Task DeleteDevice(Device device)
|
||||||
{
|
{
|
||||||
|
_devices.TryRemove(device.Id, out _);
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
@@ -207,6 +204,19 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task UpdateDevice(Device device)
|
||||||
|
{
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
dbContext.Devices.Update(device);
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_devices[device.Id] = device;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool CanAccessDevice(User user, string deviceId)
|
public bool CanAccessDevice(User user, string deviceId)
|
||||||
{
|
{
|
||||||
@@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
||||||
{
|
{
|
||||||
var caps = GetCapabilities(authInfo.DeviceId);
|
var caps = GetCapabilities(authInfo.DeviceId);
|
||||||
|
var user = _userManager.GetUserById(authInfo.UserId);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
|
||||||
|
}
|
||||||
|
|
||||||
return new DeviceInfo
|
return new DeviceInfo
|
||||||
{
|
{
|
||||||
@@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
AppVersion = authInfo.AppVersion,
|
AppVersion = authInfo.AppVersion,
|
||||||
Id = authInfo.DeviceId,
|
Id = authInfo.DeviceId,
|
||||||
LastUserId = authInfo.UserId,
|
LastUserId = authInfo.UserId,
|
||||||
LastUserName = authInfo.User.Username,
|
LastUserName = user.Username,
|
||||||
Name = authInfo.DeviceName,
|
Name = authInfo.DeviceName,
|
||||||
DateLastActivity = authInfo.DateLastActivity,
|
DateLastActivity = authInfo.DateLastActivity,
|
||||||
IconUrl = caps.IconUrl,
|
IconUrl = caps.IconUrl,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using EFCoreSecondLevelCacheInterceptor;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -16,28 +15,13 @@ public static class ServiceCollectionExtensions
|
|||||||
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
/// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
|
|
||||||
/// <returns>The updated service collection.</returns>
|
/// <returns>The updated service collection.</returns>
|
||||||
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
|
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
if (!disableSecondLevelCache)
|
|
||||||
{
|
|
||||||
serviceCollection.AddEFSecondLevelCache(options =>
|
|
||||||
options.UseMemoryCacheProvider()
|
|
||||||
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
|
|
||||||
.UseCacheKeyPrefix("EF_")
|
|
||||||
// Don't cache null values. Remove this optional setting if it's not necessary.
|
|
||||||
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
||||||
{
|
{
|
||||||
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
||||||
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||||
if (!disableSecondLevelCache)
|
|
||||||
{
|
|
||||||
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceCollection;
|
return serviceCollection;
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
|
|
||||||
<PackageReference Include="System.Linq.Async" />
|
<PackageReference Include="System.Linq.Async" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Queries;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
{
|
{
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly IServerApplicationHost _serverApplicationHost;
|
private readonly IServerApplicationHost _serverApplicationHost;
|
||||||
|
|
||||||
public AuthorizationContext(
|
public AuthorizationContext(
|
||||||
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
IDeviceManager deviceManager,
|
||||||
IServerApplicationHost serverApplicationHost)
|
IServerApplicationHost serverApplicationHost)
|
||||||
{
|
{
|
||||||
_jellyfinDbProvider = jellyfinDb;
|
_jellyfinDbProvider = jellyfinDb;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_deviceManager = deviceManager;
|
||||||
_serverApplicationHost = serverApplicationHost;
|
_serverApplicationHost = serverApplicationHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
|
var device = _deviceManager.GetDevices(
|
||||||
|
new DeviceQuery
|
||||||
|
{
|
||||||
|
AccessToken = token
|
||||||
|
}).Items.FirstOrDefault();
|
||||||
|
|
||||||
if (device is not null)
|
if (device is not null)
|
||||||
{
|
{
|
||||||
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
|
|
||||||
if (updateToken)
|
if (updateToken)
|
||||||
{
|
{
|
||||||
dbContext.Devices.Update(device);
|
await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
|
|||||||
|
|
||||||
private async Task UpdateDeviceAccess(User user)
|
private async Task UpdateDeviceAccess(User user)
|
||||||
{
|
{
|
||||||
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
UserId = user.Id
|
UserId = user.Id
|
||||||
}).ConfigureAwait(false)).Items;
|
}).Items;
|
||||||
|
|
||||||
foreach (var device in existing)
|
foreach (var device in existing)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
|
|||||||
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.UseStartup(_ => new Startup(appHost, startupConfig));
|
.UseStartup(_ => new Startup(appHost));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,18 +40,15 @@ namespace Jellyfin.Server
|
|||||||
{
|
{
|
||||||
private readonly CoreAppHost _serverApplicationHost;
|
private readonly CoreAppHost _serverApplicationHost;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly IConfiguration _startupConfig;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Startup" /> class.
|
/// Initializes a new instance of the <see cref="Startup" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appHost">The server application host.</param>
|
/// <param name="appHost">The server application host.</param>
|
||||||
/// <param name="startupConfig">The server startupConfig.</param>
|
public Startup(CoreAppHost appHost)
|
||||||
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
|
|
||||||
{
|
{
|
||||||
_serverApplicationHost = appHost;
|
_serverApplicationHost = appHost;
|
||||||
_serverConfigurationManager = appHost.ConfigurationManager;
|
_serverConfigurationManager = appHost.ConfigurationManager;
|
||||||
_startupConfig = startupConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,7 +67,7 @@ namespace Jellyfin.Server
|
|||||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||||
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
|
services.AddJellyfinDbContext();
|
||||||
services.AddJellyfinApiSwagger();
|
services.AddJellyfinApiSwagger();
|
||||||
|
|
||||||
// configure custom legacy authentication
|
// configure custom legacy authentication
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.9.8</VersionPrefix>
|
<VersionPrefix>10.9.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The identifier.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <returns>DeviceInfo.</returns>
|
/// <returns>DeviceInfo.</returns>
|
||||||
Task<DeviceInfo> GetDevice(string id);
|
DeviceInfo GetDevice(string id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets devices based on the provided query.
|
/// Gets devices based on the provided query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The device query.</param>
|
/// <param name="query">The device query.</param>
|
||||||
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
||||||
Task<QueryResult<Device>> GetDevices(DeviceQuery query);
|
QueryResult<Device> GetDevices(DeviceQuery query);
|
||||||
|
|
||||||
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
|
QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the devices.
|
/// Gets the devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
||||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||||
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
|
QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
|
||||||
|
|
||||||
Task DeleteDevice(Device device);
|
Task DeleteDevice(Device device);
|
||||||
|
|
||||||
|
Task UpdateDevice(Device device);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether this instance [can access device] the specified user identifier.
|
/// Determines whether this instance [can access device] the specified user identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
|
|||||||
|
|
||||||
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
||||||
|
|
||||||
Task<DeviceOptions> GetDeviceOptions(string deviceId);
|
DeviceOptions GetDeviceOptions(string deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,11 +64,6 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disable second level cache of sqlite.
|
|
||||||
/// </summary>
|
|
||||||
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -133,15 +128,5 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// <returns>The sqlite cache size.</returns>
|
/// <returns>The sqlite cache size.</returns>
|
||||||
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
||||||
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
|
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="configuration">The configuration to read the setting from.</param>
|
|
||||||
/// <returns>Whether second level cache disabled.</returns>
|
|
||||||
public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
|
|
||||||
{
|
|
||||||
return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.9.8</VersionPrefix>
|
<VersionPrefix>10.9.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1313,7 +1313,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
||||||
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
||||||
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
string outputPath,
|
string outputPath,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
|
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
|
||||||
|
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
|
||||||
if (shouldExtractOneByOne)
|
if (shouldExtractOneByOne)
|
||||||
{
|
{
|
||||||
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
private readonly string _encoderPath;
|
private readonly string _encoderPath;
|
||||||
|
|
||||||
|
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
|
||||||
|
|
||||||
public EncoderValidator(ILogger logger, string encoderPath)
|
public EncoderValidator(ILogger logger, string encoderPath)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -477,7 +479,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckSupportedRuntimeKey(string keyDesc)
|
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(keyDesc))
|
if (string.IsNullOrEmpty(keyDesc))
|
||||||
{
|
{
|
||||||
@@ -487,7 +489,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
|
// With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
|
||||||
|
var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
|
||||||
|
output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||||
|
|
||||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
|
||||||
|
|
||||||
// Check the Vaapi device vendor
|
// Check the Vaapi device vendor
|
||||||
if (OperatingSystem.IsLinux()
|
if (OperatingSystem.IsLinux()
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||||||
o.PoolInitialFill = 1;
|
o.PoolInitialFill = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -559,7 +561,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||||||
|
|
||||||
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
||||||
{
|
{
|
||||||
if (EnableThrottling(state))
|
if (EnableThrottling(state)
|
||||||
|
&& (_mediaEncoder.IsPkeyPauseSupported
|
||||||
|
|| _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
|
||||||
{
|
{
|
||||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||||
transcodingJob.TranscodingThrottler.Start();
|
transcodingJob.TranscodingThrottler.Start();
|
||||||
|
|||||||
@@ -898,8 +898,10 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
|
||||||
var isFirstAppliedCodecProfile = true;
|
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
||||||
|
.Reverse();
|
||||||
|
|
||||||
foreach (var i in appliedVideoConditions)
|
foreach (var i in appliedVideoConditions)
|
||||||
{
|
{
|
||||||
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||||
@@ -907,8 +909,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
||||||
{
|
{
|
||||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
|
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
|
||||||
isFirstAppliedCodecProfile = false;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -930,8 +931,10 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||||
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
|
||||||
isFirstAppliedCodecProfile = true;
|
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
||||||
|
.Reverse();
|
||||||
|
|
||||||
foreach (var codecProfile in appliedAudioConditions)
|
foreach (var codecProfile in appliedAudioConditions)
|
||||||
{
|
{
|
||||||
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||||
@@ -939,8 +942,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
||||||
{
|
{
|
||||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
|
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
|
||||||
isFirstAppliedCodecProfile = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.9.8</VersionPrefix>
|
<VersionPrefix>10.9.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
RemoveObsoleteEpisodes(item);
|
RemoveObsoleteEpisodes(item);
|
||||||
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
|
||||||
RemoveObsoleteSeasons(item);
|
RemoveObsoleteSeasons(item);
|
||||||
|
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -211,8 +211,12 @@ namespace MediaBrowser.Providers.TV
|
|||||||
}
|
}
|
||||||
else if (existingSeason.IsVirtualItem)
|
else if (existingSeason.IsVirtualItem)
|
||||||
{
|
{
|
||||||
existingSeason.IsVirtualItem = false;
|
var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
|
||||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
if (episodeCount > 0)
|
||||||
|
{
|
||||||
|
existingSeason.IsVirtualItem = false;
|
||||||
|
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.9.8")]
|
[assembly: AssemblyVersion("10.9.10")]
|
||||||
[assembly: AssemblyFileVersion("10.9.8")]
|
[assembly: AssemblyFileVersion("10.9.10")]
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
"astc",
|
"astc",
|
||||||
"ktx",
|
"ktx",
|
||||||
"pkm",
|
"pkm",
|
||||||
"wbmp"
|
"wbmp",
|
||||||
|
"avif"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.9.8</VersionPrefix>
|
<VersionPrefix>10.9.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
Overview = program.Description,
|
Overview = program.Description,
|
||||||
ProductionYear = program.CopyrightDate?.Year,
|
ProductionYear = program.CopyrightDate?.Year,
|
||||||
SeasonNumber = program.Episode.Series,
|
SeasonNumber = program.Episode.Series,
|
||||||
IsSeries = program.Episode.Series is not null,
|
IsSeries = program.Episode.Episode is not null,
|
||||||
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
|
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
|
||||||
IsPremiere = program.Premiere is not null,
|
IsPremiere = program.Premiere is not null,
|
||||||
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
[InlineData(EncoderValidatorTestsData.FFmpegV701Output, true)]
|
||||||
[InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
|
[InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
|
||||||
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
|
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
|
||||||
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
|
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
|
||||||
@@ -33,6 +34,7 @@ namespace Jellyfin.MediaEncoding.Tests
|
|||||||
{
|
{
|
||||||
public GetFFmpegVersionTestData()
|
public GetFFmpegVersionTestData()
|
||||||
{
|
{
|
||||||
|
Add(EncoderValidatorTestsData.FFmpegV701Output, new Version(7, 0, 1));
|
||||||
Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
|
Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
|
||||||
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
|
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
|
||||||
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
|
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
|
|||||||
{
|
{
|
||||||
internal static class EncoderValidatorTestsData
|
internal static class EncoderValidatorTestsData
|
||||||
{
|
{
|
||||||
|
public const string FFmpegV701Output = @"ffmpeg version 7.0.1-Jellyfin Copyright (c) 2000-2024 the FFmpeg developers
|
||||||
|
built with clang version 18.1.8
|
||||||
|
configuration: --cc=clang --pkg-config-flags=--static --extra-cflags=-I/clang64/ffbuild/include --extra-ldflags=-L/clang64/ffbuild/lib --prefix=/clang64/ffbuild/jellyfin-ffmpeg --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --enable-lto=thin --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libharfbuzz --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libopenmpt --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libvpl --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
|
||||||
|
libavutil 59. 8.100 / 59. 8.100
|
||||||
|
libavcodec 61. 3.100 / 61. 3.100
|
||||||
|
libavformat 61. 1.100 / 61. 1.100
|
||||||
|
libavdevice 61. 1.100 / 61. 1.100
|
||||||
|
libavfilter 10. 1.100 / 10. 1.100
|
||||||
|
libswscale 8. 1.100 / 8. 1.100
|
||||||
|
libswresample 5. 1.100 / 5. 1.100
|
||||||
|
libpostproc 58. 1.100 / 58. 1.100";
|
||||||
|
|
||||||
public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers
|
public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers
|
||||||
built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
|
built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
|
||||||
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317
|
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317
|
||||||
|
|||||||
Reference in New Issue
Block a user