mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-13 04:23:01 +03:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bbdfc0d49 | ||
|
|
3c3ebe8344 | ||
|
|
8c4d23435e | ||
|
|
9e4befa52e | ||
|
|
b3efae71c0 | ||
|
|
70f4f2e8c2 | ||
|
|
cd2f2ca178 | ||
|
|
8095d39954 | ||
|
|
e1ac30ba17 | ||
|
|
3b94cfa837 | ||
|
|
2fe13f54ea | ||
|
|
eb6a6d319b | ||
|
|
b74c9cae1b | ||
|
|
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="DiscUtils.Udf" Version="0.16.13" />
|
||||
<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="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||
<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="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<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.DotNetRuntime" Version="4.4.0" />
|
||||
<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.Settings.Configuration" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.9.8</VersionPrefix>
|
||||
<VersionPrefix>10.9.11</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||
{ BindToUnixSocketKey, bool.FalseString },
|
||||
{ SqliteCacheSizeKey, "20000" },
|
||||
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
|
||||
{ SqliteCacheSizeKey, "20000" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
|
||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||
|
||||
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;
|
||||
session.LastActivityDate = activityDate;
|
||||
|
||||
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>SessionInfo.</returns>
|
||||
private async Task<SessionInfo> GetSessionInfo(
|
||||
private SessionInfo GetSessionInfo(
|
||||
string appName,
|
||||
string appVersion,
|
||||
string deviceId,
|
||||
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
|
||||
return sessionInfo;
|
||||
}
|
||||
|
||||
private async Task<SessionInfo> CreateSession(
|
||||
private SessionInfo CreateSession(
|
||||
string key,
|
||||
string appName,
|
||||
string appVersion,
|
||||
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
|
||||
deviceName = "Network Device";
|
||||
}
|
||||
|
||||
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
|
||||
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
|
||||
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
||||
{
|
||||
sessionInfo.DeviceName = deviceName;
|
||||
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
|
||||
return new[] { item };
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||
private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
|
||||
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
|
||||
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 />
|
||||
@@ -1520,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
|
||||
// This should be validated above, but if it isn't don't delete all tokens.
|
||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||
|
||||
var existing = (await _deviceManager.GetDevices(
|
||||
var existing = _deviceManager.GetDevices(
|
||||
new DeviceQuery
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
UserId = user.Id
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
foreach (var auth in existing)
|
||||
{
|
||||
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
||||
|
||||
var existing = (await _deviceManager.GetDevices(
|
||||
var existing = _deviceManager.GetDevices(
|
||||
new DeviceQuery
|
||||
{
|
||||
Limit = 1,
|
||||
AccessToken = accessToken
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
if (existing.Count > 0)
|
||||
{
|
||||
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
var existing = await _deviceManager.GetDevices(new DeviceQuery
|
||||
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
UserId = userId
|
||||
}).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
foreach (var info in existing.Items)
|
||||
{
|
||||
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <inheritdoc />
|
||||
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,
|
||||
Limit = 1
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
|
||||
}
|
||||
|
||||
string? presentationUniqueKey = null;
|
||||
int? limit = request.Limit;
|
||||
int? limit = null;
|
||||
if (!request.SeriesId.IsNullOrEmpty())
|
||||
{
|
||||
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>
|
||||
[HttpGet]
|
||||
[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);
|
||||
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
|
||||
return _deviceManager.GetDevicesForUser(userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
|
||||
[HttpGet("Info")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[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)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
|
||||
[HttpGet("Options")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[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)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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.Vary, HeaderNames.Accept);
|
||||
|
||||
Response.Headers.ContentDisposition = "attachment";
|
||||
|
||||
if (disableCaching)
|
||||
{
|
||||
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
||||
|
||||
@@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
Response.Headers.ContentDisposition = "attachment";
|
||||
|
||||
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
||||
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
|
||||
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
Response.Headers.ContentDisposition = "attachment";
|
||||
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.9.8</VersionPrefix>
|
||||
<VersionPrefix>10.9.11</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
||||
private readonly ConcurrentDictionary<int, Device> _devices;
|
||||
private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
||||
@@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
_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 />
|
||||
@@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_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))
|
||||
{
|
||||
dbContext.Devices.Add(device);
|
||||
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_devices.TryAdd(device.Id, device);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
|
||||
public DeviceOptions GetDeviceOptions(string deviceId)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
DeviceOptions? deviceOptions;
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
deviceOptions = await dbContext.DeviceOptions
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(d => d.DeviceId == deviceId)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
_deviceOptions.TryGetValue(deviceId, out var deviceOptions);
|
||||
|
||||
return deviceOptions ?? new DeviceOptions(deviceId);
|
||||
}
|
||||
@@ -108,57 +121,43 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeviceInfo?> GetDevice(string id)
|
||||
public DeviceInfo? GetDevice(string id)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
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 device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
|
||||
_deviceOptions.TryGetValue(id, out var deviceOption);
|
||||
|
||||
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
|
||||
public QueryResult<Device> GetDevices(DeviceQuery query)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
IEnumerable<Device> devices = _devices.Values
|
||||
.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
|
||||
.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));
|
||||
devices = devices.Skip(query.Skip.Value);
|
||||
}
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
devices = devices.Take(query.Limit.Value);
|
||||
}
|
||||
|
||||
return new QueryResult<Device>(query.Skip, count, devices.ToList());
|
||||
}
|
||||
|
||||
/// <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>(
|
||||
devices.StartIndex,
|
||||
@@ -167,38 +166,36 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
|
||||
public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
IEnumerable<Device> devices = _devices.Values
|
||||
.OrderByDescending(d => d.DateLastActivity)
|
||||
.ThenBy(d => d.DeviceId);
|
||||
|
||||
if (!userId.IsNullOrEmpty())
|
||||
{
|
||||
var sessions = dbContext.Devices
|
||||
.Include(d => d.User)
|
||||
.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);
|
||||
if (user is null)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user is null)
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
|
||||
|
||||
return new QueryResult<DeviceInfo>(array);
|
||||
devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
|
||||
}
|
||||
|
||||
var array = devices.Select(device =>
|
||||
{
|
||||
_deviceOptions.TryGetValue(device.DeviceId, out var option);
|
||||
return ToDeviceInfo(device, option);
|
||||
}).ToArray();
|
||||
|
||||
return new QueryResult<DeviceInfo>(array);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteDevice(Device device)
|
||||
{
|
||||
_devices.TryRemove(device.Id, out _);
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().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 />
|
||||
public bool CanAccessDevice(User user, string deviceId)
|
||||
{
|
||||
@@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
AppVersion = authInfo.AppVersion,
|
||||
Id = authInfo.DeviceId,
|
||||
LastUserId = authInfo.UserId,
|
||||
LastUserName = authInfo.User.Username,
|
||||
LastUserName = user.Username,
|
||||
Name = authInfo.DeviceName,
|
||||
DateLastActivity = authInfo.DateLastActivity,
|
||||
IconUrl = caps.IconUrl,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using EFCoreSecondLevelCacheInterceptor;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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) =>
|
||||
{
|
||||
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
||||
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||
if (!disableSecondLevelCache)
|
||||
{
|
||||
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
||||
}
|
||||
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||
});
|
||||
|
||||
return serviceCollection;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncKeyedLock" />
|
||||
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
|
||||
@@ -4,7 +4,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Queries;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly IServerApplicationHost _serverApplicationHost;
|
||||
|
||||
public AuthorizationContext(
|
||||
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
||||
IUserManager userManager,
|
||||
IDeviceManager deviceManager,
|
||||
IServerApplicationHost serverApplicationHost)
|
||||
{
|
||||
_jellyfinDbProvider = jellyfinDb;
|
||||
_userManager = userManager;
|
||||
_deviceManager = deviceManager;
|
||||
_serverApplicationHost = serverApplicationHost;
|
||||
}
|
||||
|
||||
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().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)
|
||||
{
|
||||
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
dbContext.Devices.Update(device);
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
|
||||
|
||||
private async Task UpdateDeviceAccess(User user)
|
||||
{
|
||||
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
||||
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
UserId = user.Id
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
foreach (var device in existing)
|
||||
{
|
||||
|
||||
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
|
||||
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 IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Startup" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appHost">The server application host.</param>
|
||||
/// <param name="startupConfig">The server startupConfig.</param>
|
||||
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
|
||||
public Startup(CoreAppHost appHost)
|
||||
{
|
||||
_serverApplicationHost = appHost;
|
||||
_serverConfigurationManager = appHost.ConfigurationManager;
|
||||
_startupConfig = startupConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,7 +67,7 @@ namespace Jellyfin.Server
|
||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
|
||||
services.AddJellyfinDbContext();
|
||||
services.AddJellyfinApiSwagger();
|
||||
|
||||
// configure custom legacy authentication
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.9.8</VersionPrefix>
|
||||
<VersionPrefix>10.9.11</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>DeviceInfo.</returns>
|
||||
Task<DeviceInfo> GetDevice(string id);
|
||||
DeviceInfo GetDevice(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets devices based on the provided query.
|
||||
/// </summary>
|
||||
/// <param name="query">The device query.</param>
|
||||
/// <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>
|
||||
/// Gets the devices.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
|
||||
QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
|
||||
|
||||
Task DeleteDevice(Device device);
|
||||
|
||||
Task UpdateDevice(Device device);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance [can access device] the specified user identifier.
|
||||
/// </summary>
|
||||
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
|
||||
|
||||
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
||||
|
||||
Task<DeviceOptions> GetDeviceOptions(string deviceId);
|
||||
DeviceOptions GetDeviceOptions(string deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1180,28 +1180,29 @@ namespace MediaBrowser.Controller.Entities
|
||||
return info;
|
||||
}
|
||||
|
||||
private string GetMediaSourceName(BaseItem item)
|
||||
internal string GetMediaSourceName(BaseItem item)
|
||||
{
|
||||
var terms = new List<string>();
|
||||
|
||||
var path = item.Path;
|
||||
if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
if (HasLocalAlternateVersions)
|
||||
{
|
||||
var displayName = System.IO.Path.GetFileNameWithoutExtension(path)
|
||||
.Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimStart(new char[] { ' ', '-' });
|
||||
|
||||
if (!string.IsNullOrEmpty(displayName))
|
||||
var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
|
||||
if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
terms.Add(displayName);
|
||||
var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
|
||||
if (!name.IsWhiteSpace())
|
||||
{
|
||||
terms.Add(name.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (terms.Count == 0)
|
||||
{
|
||||
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
terms.Add(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,10 +180,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
}
|
||||
|
||||
public string FindSeriesPresentationUniqueKey()
|
||||
{
|
||||
var series = Series;
|
||||
return series is null ? null : series.PresentationUniqueKey;
|
||||
}
|
||||
=> Series?.PresentationUniqueKey;
|
||||
|
||||
public string FindSeasonName()
|
||||
{
|
||||
|
||||
@@ -430,8 +430,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
InternalItemsQuery query,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
var user = query.User;
|
||||
|
||||
// This must be the last filter
|
||||
if (!query.AdjacentTo.IsNullOrEmpty())
|
||||
{
|
||||
|
||||
@@ -64,11 +64,6 @@ namespace MediaBrowser.Controller.Extensions
|
||||
/// </summary>
|
||||
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
||||
|
||||
/// <summary>
|
||||
/// Disable second level cache of sqlite.
|
||||
/// </summary>
|
||||
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
||||
/// </summary>
|
||||
@@ -133,15 +128,5 @@ namespace MediaBrowser.Controller.Extensions
|
||||
/// <returns>The sqlite cache size.</returns>
|
||||
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
||||
=> 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>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.9.8</VersionPrefix>
|
||||
<VersionPrefix>10.9.11</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1313,7 +1313,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
||||
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, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
@@ -2005,7 +2005,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
|
||||
profile = WhiteSpaceRegex().Replace(profile, string.Empty);
|
||||
profile = WhiteSpaceRegex().Replace(profile, string.Empty).ToLowerInvariant();
|
||||
|
||||
var videoProfiles = Array.Empty<string>();
|
||||
if (string.Equals("h264", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
videoProfiles = _videoProfilesH264;
|
||||
}
|
||||
else if (string.Equals("hevc", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
videoProfiles = _videoProfilesH265;
|
||||
}
|
||||
else if (string.Equals("av1", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
videoProfiles = _videoProfilesAv1;
|
||||
}
|
||||
|
||||
if (!videoProfiles.Contains(profile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
profile = string.Empty;
|
||||
}
|
||||
|
||||
// We only transcode to HEVC 8-bit for now, force Main Profile.
|
||||
if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
|
||||
|
||||
@@ -89,7 +89,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
string outputPath,
|
||||
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)
|
||||
{
|
||||
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||
@@ -283,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
if (extractableAttachmentIds.Count > 0)
|
||||
{
|
||||
await CacheAllAttachmentsInternal(mediaPath, inputFile, mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
|
||||
await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -322,7 +323,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
processArgs += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -i \"{0}\" -t 0 -f null null",
|
||||
" -i {0} -t 0 -f null null",
|
||||
inputFile);
|
||||
|
||||
int exitCode;
|
||||
|
||||
@@ -168,6 +168,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private readonly string _encoderPath;
|
||||
|
||||
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
|
||||
|
||||
public EncoderValidator(ILogger logger, string encoderPath)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -477,7 +479,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckSupportedRuntimeKey(string keyDesc)
|
||||
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyDesc))
|
||||
{
|
||||
@@ -487,7 +489,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -193,7 +193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||
|
||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
|
||||
|
||||
// Check the Vaapi device vendor
|
||||
if (OperatingSystem.IsLinux()
|
||||
@@ -625,7 +625,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
try
|
||||
{
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, cancellationToken).ConfigureAwait(false);
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@@ -637,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false);
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetImageResolutionParameter()
|
||||
@@ -663,7 +663,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return imageResolutionParameter;
|
||||
}
|
||||
|
||||
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken)
|
||||
private async Task<string> ExtractImageInternal(
|
||||
string inputPath,
|
||||
string container,
|
||||
MediaStream videoStream,
|
||||
int? imageStreamIndex,
|
||||
Video3DFormat? threedFormat,
|
||||
TimeSpan? offset,
|
||||
bool useIFrame,
|
||||
ImageFormat? targetFormat,
|
||||
bool isAudio,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
|
||||
@@ -722,7 +732,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
var vf = string.Join(',', filters);
|
||||
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
|
||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter());
|
||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter());
|
||||
|
||||
if (offset.HasValue)
|
||||
{
|
||||
@@ -1197,7 +1207,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
|
||||
// Generate concat configuration entries for each file and write to file
|
||||
using StreamWriter sw = new StreamWriter(concatFilePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
|
||||
using StreamWriter sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
|
||||
foreach (var path in files)
|
||||
{
|
||||
var mediaInfoResult = GetMediaInfo(
|
||||
|
||||
@@ -501,11 +501,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
List<MediaStream> subtitleStreams,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = mediaSource.Path;
|
||||
var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i \"{0}\" -copyts",
|
||||
"-i {0} -copyts",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
@@ -676,7 +676,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
inputPath,
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
|
||||
@@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||
/// </summary>
|
||||
@@ -559,7 +561,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||
|
||||
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.Start();
|
||||
|
||||
@@ -858,7 +858,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
audioStream = directAudioStream;
|
||||
playlistItem.AudioStreamIndex = audioStream.Index;
|
||||
playlistItem.AudioCodecs = new[] { audioStream.Codec };
|
||||
playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec };
|
||||
|
||||
// Copy matching audio codec options
|
||||
playlistItem.AudioSampleRate = audioStream.SampleRate;
|
||||
@@ -897,18 +897,18 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
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)));
|
||||
var isFirstAppliedCodecProfile = true;
|
||||
i.ContainsAnyCodec(videoCodecs, 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)))
|
||||
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
||||
.Reverse();
|
||||
|
||||
foreach (var i in appliedVideoConditions)
|
||||
{
|
||||
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||
foreach (var transcodingVideoCodec in transcodingVideoCodecs)
|
||||
foreach (var transcodingVideoCodec in videoCodecs)
|
||||
{
|
||||
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
||||
{
|
||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
|
||||
isFirstAppliedCodecProfile = false;
|
||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -929,18 +929,18 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
||||
isFirstAppliedCodecProfile = true;
|
||||
i.ContainsAnyCodec(audioCodecs, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
|
||||
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
||||
.Reverse();
|
||||
|
||||
foreach (var codecProfile in appliedAudioConditions)
|
||||
{
|
||||
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
|
||||
foreach (var transcodingAudioCodec in audioCodecs)
|
||||
{
|
||||
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
||||
{
|
||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
|
||||
isFirstAppliedCodecProfile = false;
|
||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.9.8</VersionPrefix>
|
||||
<VersionPrefix>10.9.11</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -61,8 +61,8 @@ namespace MediaBrowser.Providers.TV
|
||||
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
RemoveObsoleteEpisodes(item);
|
||||
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
RemoveObsoleteSeasons(item);
|
||||
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -211,8 +211,12 @@ namespace MediaBrowser.Providers.TV
|
||||
}
|
||||
else if (existingSeason.IsVirtualItem)
|
||||
{
|
||||
existingSeason.IsVirtualItem = false;
|
||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||
var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
|
||||
if (episodeCount > 0)
|
||||
{
|
||||
existingSeason.IsVirtualItem = false;
|
||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.9.8")]
|
||||
[assembly: AssemblyFileVersion("10.9.8")]
|
||||
[assembly: AssemblyVersion("10.9.11")]
|
||||
[assembly: AssemblyFileVersion("10.9.11")]
|
||||
|
||||
@@ -102,7 +102,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
"astc",
|
||||
"ktx",
|
||||
"pkm",
|
||||
"wbmp"
|
||||
"wbmp",
|
||||
"avif"
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
38
src/Jellyfin.Extensions/FormattingStreamWriter.cs
Normal file
38
src/Jellyfin.Extensions/FormattingStreamWriter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Jellyfin.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// A custom StreamWriter which supports setting a IFormatProvider.
|
||||
/// </summary>
|
||||
public class FormattingStreamWriter : StreamWriter
|
||||
{
|
||||
private readonly IFormatProvider _formatProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FormattingStreamWriter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to write to.</param>
|
||||
/// <param name="formatProvider">The format provider to use.</param>
|
||||
public FormattingStreamWriter(Stream stream, IFormatProvider formatProvider)
|
||||
: base(stream)
|
||||
{
|
||||
_formatProvider = formatProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FormattingStreamWriter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The complete file path to write to.</param>
|
||||
/// <param name="formatProvider">The format provider to use.</param>
|
||||
public FormattingStreamWriter(string path, IFormatProvider formatProvider)
|
||||
: base(path)
|
||||
{
|
||||
_formatProvider = formatProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IFormatProvider FormatProvider
|
||||
=> _formatProvider;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.9.8</VersionPrefix>
|
||||
<VersionPrefix>10.9.11</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
Overview = program.Description,
|
||||
ProductionYear = program.CopyrightDate?.Year,
|
||||
SeasonNumber = program.Episode.Series,
|
||||
IsSeries = program.Episode.Series is not null,
|
||||
IsSeries = program.Episode.Episode is not null,
|
||||
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
|
||||
IsPremiere = program.Premiere is not null,
|
||||
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Controller.Tests.Entities;
|
||||
@@ -14,4 +17,30 @@ public class BaseItemTests
|
||||
[InlineData("1test 2", "0000000001test 0000000002")]
|
||||
public void BaseItem_ModifySortChunks_Valid(string input, string expected)
|
||||
=> Assert.Equal(expected, BaseItem.ModifySortChunks(input));
|
||||
|
||||
[Theory]
|
||||
[InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")]
|
||||
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
|
||||
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
|
||||
{
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
|
||||
.Returns((string x) => MediaProtocol.File);
|
||||
BaseItem.MediaSourceManager = mediaSourceManager.Object;
|
||||
|
||||
var video = new Video()
|
||||
{
|
||||
Path = primaryPath
|
||||
};
|
||||
|
||||
var videoAlt = new Video()
|
||||
{
|
||||
Path = altPath,
|
||||
};
|
||||
|
||||
video.LocalAlternateVersions = [videoAlt.Path];
|
||||
|
||||
Assert.Equal(name, video.GetMediaSourceName(video));
|
||||
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Extensions.Tests;
|
||||
|
||||
public static class FormattingStreamWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public static void Shuffle_Valid_Correct()
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE", false);
|
||||
using (var ms = new MemoryStream())
|
||||
using (var txt = new FormattingStreamWriter(ms, CultureInfo.InvariantCulture))
|
||||
{
|
||||
txt.Write("{0}", 3.14159);
|
||||
txt.Close();
|
||||
Assert.Equal("3.14159", Encoding.UTF8.GetString(ms.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(EncoderValidatorTestsData.FFmpegV701Output, true)]
|
||||
[InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
|
||||
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
|
||||
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
|
||||
@@ -33,6 +34,7 @@ namespace Jellyfin.MediaEncoding.Tests
|
||||
{
|
||||
public GetFFmpegVersionTestData()
|
||||
{
|
||||
Add(EncoderValidatorTestsData.FFmpegV701Output, new Version(7, 0, 1));
|
||||
Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
|
||||
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
|
||||
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
|
||||
|
||||
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
|
||||
{
|
||||
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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user